Search code examples
kotlinfunctional-programmingarrow-kt

Changing multiple attributes of a data class with a Lens


I am experimenting with Lenses in Kotlin, and I was wondering if there is an elegant way to change multiple attributes at the same time for one object. Let's say my domain looks something like this:

@optics
data class Parameters(
    val duration: Int,
    val length: Int) {
  companion object
}

@optics
data class Calculation(
    val product: String
    val parameters: Parameters) {
  companion object
}

thanks to the @optics annotations, editing single fields is easy to do:

val calculation = Calculation(product = "prod", Parameters(duration = 10, length = 15))

Calculation.product.modify(calculation) { selectedProduct }
Calculation.parameters.duration(calculation) { newDuration() }
Calculation.parameters.length(calculation) { 10 }

These lenses work perfectly in isolation, but what is the right pattern to use when I want to apply the three transformations at once? I can use a var and just overwrite calculation every time, but that does not feel very idiomatic to me.


Solution

  • Arrow currently does not expose such functionality but you can easily write a generic solution yourself.

    The snippet below demonstrates how it can be achieved, you can add additional methods to compose from Lens<S, Tuple2<FA, FB>> to Lens<S, Tuple3<FA, FB, FC>> etc.

    @optics data class Char(val name: String, val health: Int) {
      companion object
    }
    
    infix fun <S, FA, FB> Lens<S, FA>.aside(other: Lens<S, FB>): Lens<S, Tuple2<FA, FB>> = object : Lens<S, Tuple2<FA, FB>> {
      override fun get(s: S): Tuple2<FA, FB> = Tuple2([email protected](s), other.get(s))
      override fun set(s: S, b: Tuple2<FA, FB>): S = other.set([email protected](s, b.a), b.b)
    }
    
    fun main() {
      val original = Char("", 0)
      val charName: Lens<Char, String> = Char.name
      val charHealth: Lens<Char, Int> = Char.health
      val charNameAndHealth: Lens<Char, Tuple2<String, Int>> = charName.aside(charHealth)
    
      charNameAndHealth.modify(original) { Tuple2("Test", 30) }
    }