1

I have a complicated, immutable data structure that includes simple fields, but also maps and lists in the hierarchy. Maybe I'm just not reading the documentation closely enough, but there doesn't seem to be an easy way to modify the list as a whole without doing some pretty boiler-platey stuff.

For example, say I had foo.bar.list and I wanted to add an element at index i to the list. The only way I see to do that is to use the getter to get the current list, do something like

list.subList(0, i) + listOf(newElement) + list.subList(i, list.size)

and pass that to the setter.

Is there something like list.add(index, element) or list.remove(index) that you can call inside a lens to modify just the list part and keep the rest of the structure the same.

Or is there some easy way to do this with the At, Index, or Traversal parts of the Collections DSL that I just don't see?

Todd O'Bryan
  • 2,234
  • 17
  • 30

1 Answers1

2

This is possible with Arrow Optics, and Index as you've indicated. Here is a full example,

import arrow.optics.dsl.index
import arrow.optics.optics
import arrow.optics.typeclasses.Index

@optics data class Foo(val bar: Bar) {
    companion object
}
@optics data class Bar(val list: List<Int>) {
  companion object
}

val foo = Foo(Bar(listOf(1, 2, 3)))

fun main() {
  Foo.bar.list.index(Index.list(), 1).set(foo, 5)
    .let(::println) // Foo(bar=Bar(list=[1, 5, 3]))
}

You can of course also use the other operators of Optics. This example was written with Kotlin 1.6.21, id("com.google.devtools.ksp") version "1.6.21-1.0.6" and Arrow 1.1.3.

You can of course also handwrite the optics if you prefer not using Google KSP.

import arrow.optics.Lens
import arrow.optics.typeclasses.Index

data class Foo(val bar: Bar)

data class Bar(val list: List<Int>)

val bar: Lens<Foo, Bar> = Lens(Foo::bar) { foo, bar -> foo.copy(bar = bar) }
val list: Lens<Bar, List<Int>> = Lens(Bar::list) { bar, list -> bar.copy(list = list) }

val foo = Foo(Bar(listOf(1, 2, 3)))

fun main() {
  val optic = (bar compose list compose Index.list<Int>().index(1))
  val result = optic.modify(foo) { it + 3 }
  println(result)
}
nomisRev
  • 1,881
  • 8
  • 15
  • Ah, but this modifies a single element of the list. The size of the list remains the same. (Regardless, thank you for the example.) What I can't figure out how to do is to change the size of the list by adding or removing an element. – Todd O'Bryan Dec 01 '22 at 00:49
  • You can set the list on the parent with `modify { it.copy(myList = it.list + newElement) }`. – MLProgrammer-CiM Dec 01 '22 at 18:07
  • 1
    Right. So you're forced to do something like the monstrosity up above that I gave. Isn't the point of Lenses to avoid having to do these `copy` calls? I mean, a list or a set is just a data structure. Shouldn't it have a way to modify it without having to reconstruct it? – Todd O'Bryan Dec 01 '22 at 19:57
  • You can do the copy for the parent to add elements, and you can use an inner copy modified by lenses: `modify { it.copy(myList = it.list.updateByLens() + newElement) }` A lens traverses through the element, but doesn't necessarily modify the structure and for that you have to do any required operation, i.e. add. – MLProgrammer-CiM Dec 05 '22 at 17:28
  • It's sad they don't have this feature. – Risal Fajar Amiyardi Jan 31 '23 at 14:57