Wednesday, July 18, 2018

Why we switched to Kotlin (Pt. 2) - Immutable/Mutable Collection Interfaces


In the previous post I explained that the reason our team made the jump to Kotlin was primarily because of Kotlin's ability to express business and architectural decisions in a much clearer way than Java can. In the last post I focused on nullability as a key business and architectural decision which, regretablly, has no top level expression in Java but which is a first class concept in Kotlin. In this post I will discuss how immutability, in particular with regard to collections, is also an important design decision which is buried in the implementation in Java but are surfaced in signatures in Kotlin. In the next post I will compare immutability with regard to objects in Java and Kotlin.

Although decisions regarding immutability are often less business decisions than architectural ones, stating them explicitely in the language does allow one to reason about code and understand the intention in software much better. Take for example the following ambiguous signature in Java.

    public List<User> removePreferredUsers (List<User> list)
   

Does the method modify the list I give it? Is the returned list a new list or the same list but now with preffered users removed? Assuming that my intention is to have a list of all users and a list with preferred users excluded, there really would be no way to know whether it is safe to pass in the list of all users or whether we have to create a copy first. Methods like these give me new appreciation for the functional programming constraint that one not modify the inputs. To know that the method is respecting that functional paradigm, I would have to read through the implementation, read possibly existent and possibly accurate Javadocs or write exploratory unit tests. The implementer of this method almost certainly did not mean to communicate that the List should be modifiable. It is merely the default collection interface, the least number of keystrokes.

Not only that but, since the constraint is not enforced at compile time, it is possible that at one point the method was not mutating the list whereas, on a later update, it now is. This could lead to bugs that are very difficult to trace down. It is true that we could merely change the signature to UnmodifiableList and force the client code to wrap the list, but I have never seen this done in practice. It is just too unweildy and, honestly, mutation of arguments is not something people really stop and think about (me included). They just generally assume one way or the other (probably the same reason I have never seen a method take an Optional as an argument, though it is arguably Java's answer to the problem of nullability).

Josh Bloch in his signature book Effective Java states two principles in Item 15 regarding mutability, "Classes should be immutable unless there's a very good reason to make the mutable" and "If a class cannot be made immutable limit its mutability as much as possible" (Kindle Location 1876). He presents the case for these principles more effectively than I could and I recommend you read it. A good language is one that makes best practices easier than the alternative and Java, in this case, does not.

In Kotlin, this method would looks something like this:

    fun removePreferredUsers(List<User> list) : List<User>
   
or this:

    fun removePreferredUsers(MutableList<User> list) : List<User>
   
The List interface, like the Set and Map interfaces, are immutable by default in Kotlin, hiding the mutating methods found in their corresponding interfaces in Java. The intention is much clearer in each of the Kotlin signatures. The first indicates that, at least in the case that there is one or more preferred users, it will return a new list. It guarantees that it will not change the original. Of course, for reasons stated above, I would prefer the first to the second but at least with the second I know what I am exposing the list to and I can make a copy to pass in as an argument if I want to keep the original list unmodified. The implementer of the method chose to add more keystrokes for a reason to arrive at MutableList and that communicates a clear intention to modify.

Using the List interface in the Java example gives us no real indication as to the intentions of the implementer of that method regarding whether the method plans to modify the original argument whereas the signatures in Kotlin do communicate this decision, enforce it at compile time and, as a bonus, make the default implementation the safer one.

No comments:

Post a Comment