Kotlin

Kotlin: Mutability

Kotlin is a very rich language with much subtlety tucked away. In this occasional series (meaning that there will be occasional, standalone posts covering distinct areas) we’ll explore some of these subtleties. In this post we’ll take a look at mutability.

Mutability is a core concept in Kotlin, but all is perhaps not what it seems. The fundamental concept here is if we declare variables using var then they are mutable and can be reassigned with another value, whereas if we declare variables using val then they are immutable and cannot be reassigned. However, it is important to remember that var and val only control the variable itself, and not the object instance that is assigned. We can illustrate this with a simple example:

data class MyData(var value: String)
...
var mutable = MyData("Foo")
val immutable = MyData("Bar")

mutable = immutable    // This is fine
immutable = mutable    // This will not compile because
                       // immutable cannot be reassigned

We can happily change mutable as much as we like, but we cannot reassign immutable with a different value. This may give the impression that we cannot change immutable, but that is actually not the case. While the variable itself is immutable, the instance of MyClass that it references is mutable:

immutable.value = "Foo"    // This is fine

At first this feels a little counter intuitive, but it makes perfect sense when we consider what is actually going on. While the variable named immutable cannot be changed it references a data class which has a single field named value which can be changed because it is declared as a var.

If we want to make the variable named immutable truly behave as an immutable object then we would need to change the implementation of MyClass itself to be immutable by making its property named value immutable by declaring it as a val:

data class MyData(val value: String)

On the face of it this would appear to now break the mutability of the variable named mutable but this is not the case. While we can no longer directly alter the value property of the MyClass instance that it references, we can assign it with a new instance of MyClass which as a different value property:

var mutable = MyData("Foo")

mutable = MyData("Bar")    // This is fine

Of course there is the overhead of having to create a new object each time, but it does give us a much cleaner and safer mutability contract. It is for this reason that we should strive for immutability wherever possible because it is easier to relax an immutable object to a mutable one (through new instance creation) than it is to tighten a mutable object to be an immutable one.

We can leverage Kotlin sealed classes to allow us to create mutable and immutable variants of the same underlying class:

sealed class MyValue(default: String) {

    open val value: String = default    

    class Immutable(default: String): MyValue(default) {
        fun mutate(): Mutable = Mutable(value)
    }

    class Mutable(default: String) : MyValue(default) {
        fun immutate(): Immutable = Immutable(value)

        override var value = default
    }
}

It is not possible to directly create an instance of MyValue, instead we create an instance of either MyValue.Immutable or MyValue.Mutable. The underlying value property in MyValue is a val, but we override this in MyValue.Mutable to make it a var. Kotlin allows us to override a val property in the base class as a var, but not the other way around.

We can also include some convenience methods which allow us to convert between mutable an immutable variants of the same base class.

Using this quite simple pattern we get concrete enforcement of our mutability contracts at compile-time:

val mutable = MyValue.Mutable("Foo")
val immutable = MyValue.Immutable("Foo")
    
mutable.value = "Bar"    // This is fine
    
immutable.value = "Bar"  // This will not compile because 
                         // value cannot be reassigned
    
val mutated = immutable.mutate()
mutated.value = "Bar"    // This is fine

immutable.value = "Bar"  // This will not compile because
                         // value cannot be reassigned

val immutated = mutated.immutate()

immutated.value = "Bar"  // This will not compile because
                         // value cannot be reassigned

It is also worth mentioning that there’s a really nice trick that we can use to make a var property immutable:

class MyOtherValue(default: String) {
    var value: String = default
        private set
}

Even though the property named value is declared as a var, we can make its setter private and it will behave as though it was declared are a val:

val myOtherValue = MyOtherValue("Foo")

myOtherValue.value = "Bar"  // This will not compile because
                            // value cannot be reassigned

Kotlin provides us with some nice, clean mechanisms for controlling the mutability of objects, but we also need to think carefully about how we structure or classes to provide consistency.

As the concepts covered here are largely self-contained, and the code snippets can be copied / pasted directly there is no accompanying source code repo for this article.

© 2018 – 2022, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

4 Comments

  1. The underlying value property in MyValue is a var, but we override this in MyValue.Mutable to make it a var. Kotlin allows us to override a val property in the base class as a var, but not the other way around.

    I think it should be “The underlying value property in MyValue is a val”.

  2. Great pattern, I tried it out for one of my class models for representing a discard pile in Mahjong and I really like it!

    “`
    package models

    import java.util.*

    sealed class DiscardPile(protected val tiles: Stack) {
    init {
    // Push an initial tile so that there’s always a tile.
    tiles.push(Tile.NONE)
    }

    fun peekRecentlyDiscardedTile(): Tile {
    val discardedTile = tiles.peek()!!
    require(discardedTile != Tile.NONE)
    return discardedTile
    }

    fun getTiles(): List {
    return tiles.filter { it != Tile.NONE }.toList()
    }

    fun makeString(): String {
    return tiles.filter { it != Tile.NONE }.windowed(15, step = 15, partialWindows = true)
    .joinToString(separator = “\n”) { it.joinToString() }
    }

    protected fun tilesCopy(): Stack {
    val stackCopy = Stack()
    stackCopy.addAll(tiles)
    return stackCopy
    }

    class Mutable(tiles: Stack? = null) : DiscardPile(tiles ?: Stack()) {
    fun takeRecentlyDiscardedTile(): Tile {
    val discardedTile = tiles.pop()!!
    require(discardedTile != Tile.NONE)
    return discardedTile
    }

    fun accept(tile: Tile) {
    require(tile != Tile.NONE)
    require(!tile.isFlowerTile())
    tiles.push(tile)
    }

    fun immutable(): Immutable {
    return Immutable(tilesCopy())
    }
    }

    class Immutable(tiles: Stack? = null) : DiscardPile(tiles ?: Stack()) {
    fun mutable(): Mutable {
    return Mutable(tilesCopy())
    }
    }
    }
    “`

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.