Pattern for composing while maintaining an audit trail

Background here is I have a personal Elm app for running finance/budget simulations.

I had an Accounts type, and everything was built on scenarios which had a number of Steps

type alias Step = Constants -> Date -> Accounts -> Accounts

That is, a Step can look at the date, constants, current state of accounts, and apply a transfer function to create an updated Accounts.

All good, but I had no audit trail of the sequence of transfers that occurred. I didn’t want to burden the step implementations with logging, and I wanted to prevent composing Accounts -> Accounts. So I un-exposed Account.transfer and I created an explicit Transfer type, and required Step to be

type alias Step =
    Constants -> Date -> Accounts -> List Transfer

Now a step could never be tempted to compose other steps and destroy the information about the sequence of transfers.

But it was so nice to have the old Step type be composable:

compose : List Step -> Step
compose steps =
    \constants date ->
        steps
            |> List.map (\step_ -> step_ constants date)
            |> List.foldl (>>) identity

but that was no longer true.

Then here comes the epiphany that came as a bit of a surprise, and seemed a bit “dirty” and wrong, but…

I can compose multiple Accounts -> List Transfer.

type alias Update =
    Accounts -> List Transfer


andThen : Update -> Update -> Update
andThen nextUpdate prevUpdate =
    \accounts ->
        let
            prevTransfers : List Transfer
            prevTransfers =
                prevUpdate accounts

            intermediateAccountsValue : Accounts
            intermediateAccountsValue =
                prevTransfers |> List.foldl applyTransfer accounts

            nextTransfers : List Transfer
            nextTransfers =
                nextUpdate intermediateAccountsValue
        in
        prevTransfers ++ nextTransfers

So now I can compose updates to accounts, but never lose the sequence of transfers that occurred.

The question for the more knowledgeable, is What is this pattern called"? This general idea of representing operations that are conditional on input (input -> operations), and composing them so as to maintain an audit trail of all operations that occurred?

(Note: I realize I’m applying transfers repeatedly in composing, and I’ll try to fix that later.)

3 Likes

Interesting post and question. I was about to say that you have re-invented the Writer Monad but its not really true because the type signature of your andThen.

As far as I understand it would be possible achieve what you want with a Writer Monad approach though.

Then you would have to implement andThen with a signature like this:

andThen : (Accounts -> (Accounts, List Transfer)) ->  (Accounts, List Transfer) -> (Accounts, List Transfer)
andThen f a = ....

You would also need some function to “run and apply” the result from your update functions:

run : (Accounts -> List Transfer) -> Accounts -> (Accounts, List Transfer)
run f accounts = ...

Then you should be able to do something like:

(initalAccount, [])
  |>  andThen (run myUpdate1)
  |>  andThen (run myUpdate2)

The result contains the updated accounts and the list of transactions. Hope this helps?

Btw, you are also using the Reader Monad pattern when your threading Constants and Date through the function calls. Nice! :slight_smile:

2 Likes

Thanks for the valuable feedback @konnik.

Seems that staying with the Accounts -> List Transfer type is the most direct way of expressing my intention, but I really appreciate seeing how i can optimize this if/when that becomes necessary.

I think a mix of the two approaches could be best. Implementations of Step could use my andThen for expressivity, whereas my Scenario module that runs all of the steps over multiple months/years could use your more efficient approach.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.