I know very little about functional programming but can see its appeal, but I wonder, whether in practice you really want to "build the world from scratch" every time you need an account balance.
I understand that the way to model an account balance is to store each transaction (deposits and withdrawals) immutably, and then implement a balance function = sum(deposits)-sum(withdrawals). But I'm wondering if this is the way you would model it practice, say on a website that displayed a customer's bank balance.
Would it perhaps make sense to save a checkpoint (e.g. balance at every nth transaction), and then when somebody wants to get a balance, you model the function is balance = checkpointBalance[checkpointBalance.length-1] + sum(deposits).where(index>checkpointIndex) - sum(withdrawals),where(index>checkpointIndex)
. (I guess the idea would be set n
to a number which appropriately balances storage of checkpoints cost against cost/speed of summing n transactions?) Or something along those lines? Or does that violate the spirit of functional programming?
There's more than one way to do that. In a traditional bank account, the aggregated value (the balance) is recalculated for every transation.
In other systems, the history may be sufficiently shallow that you can easily recalculate it from scratch whenever you need it.
If you have an aggregate with a particularly long and convoluted history, you can do as you suggest. In Event Sourcing literature, this is typically called a Snapshot.
Find the most recent Snapshop and only replay the events that occurred after the Snapshot.
The beautiful thing about an immutable append-only history is that you can always add the Snapshots as an afterthought. You don't have to design that optimization in from the start. You can even have a background worker that asynchronously creates Snapshots if required.
Now, granted, saving a Snapshot somewhere is a side effect, but so is saving each of the events as they occur. Even in FP, you can't avoid some side effects. The trick is to minimise them.
You can consider Snapshots as essentially persisted memoisation. And again, you might argue that memoisation is impure (because it involves the ostensible side effect of mutating some internal map of inputs to values), but even if impure, a memoised function would still be referentially transparent because it would always return the same result for the same input with no observable side effects.