Who is afraid of immutable data?
Anand Kumar Keshavan Founder/Partner Swanspeed Consulting
Lately, my LinkedIn feed has been getting posts about events on functional programming with special emphasis on how to deal with immutable data .
True, many programmers are puzzled when told that from now onwards you shall not modify the value of a variable once it has been initialised. "But how can I work with loops? Doesn't incrementing a counter need the value of a counter to be changed?" This is just the result of doing something over and over again and can be overcome by learning some programming techniques such as recursion and map/reduce over collections. The larger problem, however, lies with the questions of the following nature: "What about real world things like account balances ? How do I change the account balance of I am not allowed to change the value?". ( To be fair to programmers who are new to the functional world , such questions have been raised by some eminent thought leaders in YouTube videos while expressing their reservations about functional programming)
This post addresses the second class of questions, as these represent some fundamental questions about the way we look at application domains.
What I am about to say might come as a surprise to many programmers, but this has been obvious to accountants and finance professionals for centuries. In the real world, transactions are immutable. When we deposit money into a bank account, the transaction cannot be changed.. we cannot go back in time and change the amount we deposited into the bank. This is so obvious and banal that we don't even consciously think about it anymore. The only way to change the transaction is to create another transaction which negates the the first one. If we one could change the transaction by any other means, one's bank balances will have no integrity or meaning. Like I said, accountants have known this for centuries and literally wrote the book on this topic. ( Perhaps that is the reason they rule the world!!) This is true for ordering a device from Amazon or one's attendance data at school or a work place.
In the real world there is a temporal angle to objects such as accounts and balances. When one says that " my account has a balance of Rs. xxx", one means that "my account has a balance of Rs. xxx at time(t), where t is the current time. But one may have a different balance at another time(t), say last Monday. The important thing to note here is that for a given account "a balance at time(t)" is immutable. Therefore a a function such as getBalanceAt(Time t) will be a pure function i,e, each time you call it it will return the same value. This property also makes the function referentially transparent. Both properties are foundational elements of functional programming.
Okay, let us model the aforesaid account/ balance problem using immutable data types. Normally, I would use a functional language such as Scala or Haskell, but in this case I have used Java ( yes, plain old Java that too,, none of the functional extensions in Java 8). Why? Because it will be comprehensible to a larger audience and Scala/Haskell programmers probably don't have a problem with immutability anyway.
Unfortunately, trying to do this in Java we run into a problem almost immediately- the data structures such as ArrayList<T> in Java are mutable. One could use collections.unmodifable() function to make lists immutable. But the interface is clunky and unwieldy. Therefore here is a basic immutable lisp-like list class:
IList.java
public class IList<T> { final T hd; final IList<T> tl; public IList(T hd) { this.hd=hd; this.tl=new IList<>(); } public IList(){ this.hd=null; this.tl=null; } public IList(T hd,IList<T> tl){ this.hd=hd; this.tl=tl; } public T head() { return hd; } public IList<T> tail() { return tl; } public IList<T> add(T value){ return new IList<>(value,this); } public Boolean isEmpty() { return this.head()==null; } @Override public String toString() { return "IList ("+ hd + ", tl=" + tl + ")"; } }
This is a fairly trivial implementation of a simple immutable list. A serious implementation of a List class will have traversal functions, append, reverse, map and reduce functions. In our example we will just be using the constructors and the isEmpty() function. For those who are more academically inclined you can visit the author's github page for a purely functional implementation of immutable lists in Javascript. The more adventurous ones are invited implement this in Java. An in-depth treatment of functional data abstractions is available in the second part of my series on functional programming.
We need another class to model time. Here is a fairly simple implementation that simulates time as an ever increasing integer ( increases by 10 each time next() is called. These kind of objects are required to write automated tests. In real programming scenarios, one might consider dependency injection of the a class that models time- but that is not really within the scope of this post.
ITime.java
public class ITime { private static final Integer incr=10; final Integer value; private ITime(Integer v){ this.value=v; } public ITime next() { return new ITime(this.value+incr); } public static ITime start() { return new ITime(0); } public static ITime from(Integer i) { if(i<0) return ITime.start(); else return new ITime(i); } public Boolean greaterThan(ITime that) { return this.value>that.value; } public Boolean lessThan(ITime that) { return this.value<that.value; } public Boolean lessThanEqualTo(ITime that) { return this.value<=that.value; } public Boolean greaterThanEqualTo(ITime that) { return this.value<=that.value; } @Override public String toString() { return "ITime [value=" + value + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ITime other = (ITime) obj; if (value == null) { if (other.value != null) return false; } else if (!value.equals(other.value)) return false; return true; } }
The ITime also has equals, lessThan, greaterThan... comparison operations, although we use just use one of them in our Account() class.
Alright, back to our Account/Balance problem.
An account has :
- an Opening balance
- A list of transcations
- Balance
- and most importantly a time element
The snapshot of an account at any given point of time is immutable-- its opening balance( same at every point in time), List of transactions, which is constant for any specific point in time and so on.
Here are the classes that mirror the Transaction entities and are straight forward OO representations.
public abstract class Transaction { private final String description; private final Double amount; private final ITime time; Transaction(String desc,Double amount, ITime time){ this.description=desc; this.amount=amount; this.time=time; } String getDescription() { return description; } Double getAmount() { return amount; } abstract Double getComputationalValue(); ITime getTime() { return time; } } //Debit.java public class Debit extends Transaction{ Debit(String desc, Double amount,ITime time){ super(desc,amount,time); } @Override public Double getComputationalValue() { return this.getAmount()*-1; } } //Credit.java public class Credit extends Transaction{ Credit(String desc, Double amount,ITime time){ super(desc,amount,time); } @Override public Double getComputationalValue() { return this.getAmount(); } }
Our account has a list of Transactions. The class hierarchy given above, models debit/credit transaction types. Please note that each transaction has a time element. Also the values of time and amount are declared to be final, making them immutable. There is a function called getComputationalValue() that seems kind of out of place. For Debit transactions it returns the negative value for the amount , while for Credit() transactions it returns a positive value. This is used to model the idea that a debit() reduces the balance, while a credit() increases the balance. This is not the ideal way to model these concepts- but for the purposes of this post, which is about immutability, this should be fine.
And here is the Balance type which represents the balance at a point in time.
Balance.java
public class Balance { final Double amount; final ITime time; Balance(Double amount,ITime time){ this.amount=amount; this.time=time; } @Override public String toString() { return "Balance [amount=" + amount + ", At time=" + time + "]"; } }
As straight forward at it gets. The purpose of this object is to define the signature of getBalance() functions in the Account class. These functions, as you shall see, always return a "balance at" some "time". Internally, the balance for an account is maintained as a double.
And finally, the Account() class:
Preview:
public class Account { final Integer id; final Double openingBalance; final ITime openingTime; final IList<Transaction> transactions; final ITime lastUpdatedTime; final Double balance; private Account(Integer id, Double opbal) { this.id=id; this.openingBalance=opbal; this.balance=opbal; this.transactions=new IList<Transaction>(new OpenAc(ITime.start()),new IList<>()); this.lastUpdatedTime=transactions.head().getTime(); this.openingTime=this.lastUpdatedTime; } private Account(Account that, Transaction transaction) { this.id=that.id; this.openingBalance=that.openingBalance; this.openingTime=that.openingTime; this.transactions=new IList<>(transaction, that.transactions); this.lastUpdatedTime=transaction.getTime(); this.balance=that.balance+transaction.getComputationalValue(); } private Account(Account that, Double balance, IList<Transaction> transactions) { this.id=that.id; this.openingBalance=that.openingBalance; this.openingTime=that.openingTime; this.transactions=transactions; this.balance=balance; this.lastUpdatedTime=transactions.head().getTime(); } private Account applyTransaction(Transaction transaction) { return new Account(this,transaction); } private Account previous() { if(this.lastUpdatedTime.equals(this.openingTime)) return null; final Transaction lastTransaction= transactions.head(); final double previousBalance= this.balance-lastTransaction.getComputationalValue(); return new Account(this,previousBalance,this.transactions.tail()); } private static Account snapShotAtTime(Account ac, ITime time) { if(ac.lastUpdatedTime.lessThanEqualTo(time)) return ac; return snapShotAtTime(ac.previous(),time); } public static Account open(Integer id, Double opBal) { return new Account(id, opBal); } public Optional<Account> debit(String desc,Double amount) { final ITime newTime=this.lastUpdatedTime.next(); if(amount>this.balance) return Optional.empty(); else return Optional.of(this.applyTransaction(new Debit(desc,amount,newTime))); } public Optional<Account> credit(String desc,Double amount){ final ITime newTime=this.lastUpdatedTime.next(); return Optional.of(this.applyTransaction(new Credit(desc,amount,newTime))); } public Balance getBalance() { return new Balance(this.balance,this.lastUpdatedTime); } public Balance getBalanceAt(ITime atTime) { if(atTime.value>this.lastUpdatedTime.value) return this.getBalance(); else return snapShotAtTime(this,atTime).getBalance(); } }
Here are some salient points about this class:
- all data members are immutable
- all constructors are private- clients can construct an account only by calling the factory type static function open(). ( This is very close to the domain-- you can only create an account by opening it, right?)
- it allows two operations "debit" and "credit" which also closely model the domain. Both these functions retuning a Optional<Account> where, if present, the Account() has new value for time. If you see the underlying private function, applyTransaction() , it returns a new Account with the updated transaction list
- Transactions, once added cannot be removed by the client of this class. Within the class, they can go to the previous snapshot by "subtracting" the current transaction.
- The private function snapshotAt Time ( t) gives us the snapshot of the account for a given value of t.
- the public function getBalance() returns the latest balance.
- the public function getBalanceAt(t) returns a the balance at some time t.
Due to its immutable nature one can reason about an Account object. The opening balance plus the sum of all transactions will always be equal to the balance. This is an equation and one can write a self verifying function which assures this after each transaction. Similarly, the reverse operation is also true. If you subtract all the transactions from the current balance one you will get the opening balance. What happens if you maintain two copies of this account, where one of the copies verifies using the first technique and the other one using the second technique? You get something very similar to a very elementary type of blockchain.
Modelling domain types as immutable elements ( which, in all probability are, in real life) has great benefits. It allows one to reason about the functions and test them with more precision. Also, without immutability, one is likely to get into some serious trouble if one tries to move into concurrency. Unfortunately, some of the foundational elements of any tech stack, RDMSs, for example, do not support immutability, a legacy from a time when storage was expensive. A lot of checks and balances have to be created to overcome this limitation of storage software. The newer databases such as Datomic support immutability out-of-the-box. One hopes that in the near future, such ideas will propagate to the mainstream DBMS software.
[This is not the best way to implement these kinds of types in a functional language such as Scala, for there are much more elegant ways of doing so. The purpose of this post is to help the reader to cross the mental barrier of immutability! ]
Comments
Post a Comment