Immutability for the Masses

20 Nov 2013

Immutability can have its advantages, but a language and standard library has a large impact on whether or not immutable solutions are even considered to a given problem. Poor interop between mutable and immutable APIs within a platform can cause people to eschew one (generally immutability because of its poorer performance) even when there may have been some advantage. For example, we can look at the difference between Date and String in Java:

public class Event {
    private Date date;
    private String title;

    public Event(Date date, String title) {
        this.date = date;
        this.title = title;
    }

    public Date getDate() {
        return this.date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

To the untrained eye, this looks perfectly fine until you realize that java.util.Date has a deeply mutable API and we will not know when that mutation will happen. If you want to limit mutation to only the get methods, you have to defensively clone every date object you're given.

public class Event {
    private Date date;
    private String title;

    public Event(Date date, String title) {
        this.date = (Date)date.clone();
        this.title = title;
    }

    public Date getDate() {
        return (Date)this.date.clone();
    }

    public void setDate(Date date) {
        this.date = (Date)date.clone();
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

Now we can rest assured that at no time will the date of the event be changed without using a set function. If we need to add validation or some post-modify code, we can simply hook into the setter.

If you notice, I'm not doing the same thing with the String, why is that? Well, the java.lang.String API is completely immutable (well, without reflection). This immutability gives us leverage to share strings freely, with an agreement that nobody tries to get cute and blow the whole thing up.

Trade-offs

So, why would one use mutability or immutability? The answer depends on the use-case. For many applications, mutable data structures are more efficient than their immutable counterparts. Unfortunately, on large systems the mutability can make it much more difficult to track down causal links between different parts of the code.

Using the mutable example above, if you had a bug where an Event was getting the wrong date in some cases, you would not only have to look everywhere Event.setDate was used, you would also need to make sure you track down every use of a setter on java.util.Date. This can be an insurmountable task, depending on the frequency of Date objects.

On the other hand if you use the immutable example above, the constant copying can begin to wear on performance. Many of the copies will never be mutated, meaning that the clone was in vain.

An Elegant Weapon for a More Civilized Age

How then, should we proceed with creating APIs that other programmers will be saddled with? The easy answer is that mutability doesn't preclude a user from doing the requisite work to safely share instances across the application. For this reason, most Java APIs allow mutation unless it is very clear that the object represents a value.

If we look back, C++ offers a very interesting solution to this problem of the mutable/immutable duality: const

public class Event {
private:
    time_t date;
    char *title;
public:
    &time_t GetDate() { return this->date }
    char *GetTitle() { return this->title }


    time_t GetDate() const { return this->date }
    const char *GetTitle() const { return this->title }
}

This allows us to pass an object of type const Event to functions that should not modify the date or title of the object and an Event to functions that should.

Who Decides?

The problem with the const solution is that an object can simultaneously be viewed as const and non-const. That is the beginning of a sticky situation in managed languages where most things are passed by reference. If you added a const syntax to Java, we would still have the same problems because if a const reference is stored and later somebody with non-const access to the object decides to mutate it, we again have no idea whether we can rely on the object without a full copy.

Ultimately the entire world has to agree at any time whether an object is mutable or immutable for any of the benefits of immutability to hold. The ideal situation, therefore is that a language has a way to freeze an object when it is being passed to a function expecting an immutable object and to thaw an object by cloning it when it needs to be mutable again. This way, we can count on immutable values never being changed and we can build compiler/vm optimizations to elide copies when they aren't necessary. This allows the user to gain performance by using mutable objects when necessary to perform multiple updates and to share objects when no mutation is necessary.