How to fix the pain of modifying Kotlin nested data classes

When Kotlin arrived a few years ago one of the selling points was immutability:

In object-oriented and functional programming, an immutable object (unchangeable object) is an object whose state cannot be modified after it is created.

https://en.wikipedia.org/wiki/Immutable_object

This is very cool! Value objects are such a huge help when it comes to reasoning about code, safety and avoiding weird subtle misbehaviours due to “things changing unexpectedly under the hood“.

When you have a value object you have properties that are, well, immutable, so you don’t have setters. To alter a property you need to create a new copy of the object with the property updated.

To make things a bit more comfortable, Kotlin compiler adds a .copy() method to every data class and we can use this method to easily create a new version of the original object that has updated properties

data class Account(val name: String, val amount: Int)

fun main() { 
  val account = Account("Frank", 100) 
  println(account) 

  val updated = account.copy(amount = 90)
  println(updated)
}

So far so good! Everything is super cool if you have simple data class with a few properties, even better if they are primitive types. Things start to get cumbersome when we start nesting data classes with data classes.

In the next example, we want to create a new copy of account and we want to update only the mobile phone number:

data class Bank(val name: String, val accountNumber: String)

data class Phone(val mobile: Mobile, val home: Home)
data class Mobile(val prefix: String, val number: String)
data class Home(val prefix: String, val home: String)
data class Address(val address: String)
data class ContactInfo(val phone: Phone, val address: Address)

data class Account(val name: String, val bank: Bank, val contacts: ContactInfo)

fun main() {
  val account = Account(
    name = "Frank",
    bank = Bank("SuperBank", "qwertyuio"),
    contacts = ContactInfo(
      phone = Phone(
        Mobile("+39", "123456789"),
        Home("+39", "987654321")
      ),
      address = Address("In the middle of nowhere")
    )
  )

  println(account)

  val updatedAccount = account.copy(
    contacts = account.contacts.copy(
      phone = account.contacts.phone.copy(
        mobile = account.contacts.phone.mobile.copy(number = "000000")
      )
    )
  )

  println(updatedAccount)
}

That’s a lot of copy copy copy 🤦🏻‍♂️ Don’t get me wrong: things are still doable. We are programmers after all, we can do everything, it’s just a matter of time and pain. I like to use “pain” as a term because after a while that’s what I feel working with “the wrong tool”. It’s not physical pain, of course, but it’s more like “morale” or “enthusiasm” pain. It’s the idea of being like:

Oh fuck! Again that class with the 200 copy calls 🤦🏻‍♂️

We didn’t have this type of problem in the past because with Java we were simply changing things all the time #yolo. Immutability was some fancy thing available maybe via some library that we didn’t have or we didn’t care about.

Over time we realised that changing things all over the place was one of the causes of the worst subtle bugs and we started to appreciate immutability. Then Kotlin came with data classes and we were like

but also

There must be a way to ease the pain. It turns out there is.

We can use a Lens

val updatedAccount = Account.contacts.phone.mobile.number.modify(account) { "000000" }

That’s basically it 😂 I’m serious! No .copy, no nesting, no keeping track of what to keep and what to change.

You start typing from the original type name, i.e. Account and you keep typing dot property dot property dot property until you reach the property that you want to change and then pass a lambda specifying how to change the value of the property. Everything with one line 😃

Just to iterate:

// We went from this
val updatedAccount = account.copy(
    contacts = account.contacts.copy(
      phone = account.contacts.phone.copy(
        mobile = account.contacts.phone.mobile.copy(number = "000000")
      )
    )
  )

// to this
val updatedAccount = Account.contacts.phone.mobile.number.modify(account) { "000000" }

How do we do that?

Lenses are not part of the Kotlin Standard Library yet, so we will need to use an Open Source library called Arrow-kt. Arrow-kt is a huge community effort to bring some of the missing functional programming features to Kotlin. 

Setup

Well, nothing fancy here. We need a new dependency repository in your build.gradle. You should already have jcenter and probably google if you do Android. Simply add the arrow-kt one as below:

repositories {
    google()
    jcenter()
    maven { url "https://dl.bintray.com/arrow-kt/arrow-kt/" }
}

You also need kapt. If you are doing Android you probably already have something like

apply plugin: 'kotlin-kapt'

in your build.gradle. Final touch, in your dependencies block add the Arrow-kt dependencies you need for this:

def arrow_version = "0.11.0"
dependencies {
   [...]
   implementation "io.arrow-kt:arrow-optics:$arrow_version"
   kapt "io.arrow-kt:arrow-meta:$arrow_version"
}

Let’s make it happen!

To enable our data classes to have Lenses we need to mark them with the @optics annotation:

import arrow.optics.optics

@optics
data class Account(val name: String, val bank: Bank, val contacts: ContactInfo) { companion object}

@optics
data class ContactInfo(val phone: Phone, val address: Address) { companion object }

@optics
data class Phone(val mobile: Mobile, val home: Home) { companion object }

@optics
data class Mobile(val prefix: String, val number: String) { companion object }

data class Bank(val name: String, val accountNumber: String)
data class Home(val prefix: String, val home: String)
data class Address(val address: String)

Due to an unfortunate limitation in Kotlin, we are force to add a companion object to those classes. Kotlin and Arrow are constantly evolving and this particular annoyance is temporary and we won’t need it in 2021. 

Wrapping up

This is pretty much it 😂 

Today we learned that if you have a nested data class due to your complex UI State object or some complex JSON object and you find it painful to modify, you can create a Lens 👓 for it and modify it with one line 😍

See you soon 💪🏻