Is an `Index` the idiomatic way of focusing down o...
# arrow
s
Is an
Index
the idiomatic way of focusing down on a specific item in a list based on a criteria (e.g. “find by ID”), or is there something I haven't realized yet? We find ourselves needing to do this quite often, and we've resorted into essentially this pattern:
Copy code
@optics data class Lesson(val id: String, val name: String) {
    companion object
}

@optics data class Lessons(val items: List<Lesson>) {
    companion object
}

fun main() {
    val lessons = Lessons(listOf(Lesson("1", "Lesson One"), Lesson("2", "Lesson Two")))

    Lessons.items.index(atLessonId, "1").name.set(lessons, "New Lesson One Name")
}

private val atLessonId = Index<List<Lesson>, String, Lesson> { id ->
    Optional(
        getOption = { lesson: List<Lesson> ->
            lesson.firstOrNone { it.id == id }
        },
        set = { lessons: List<Lesson>, lesson: Lesson ->
            TODO("Not implemented for brevity")
        },
    )
}
a
Lessons.items.filter { it.id == id }.name.set(...)
s
Right, I guess I did actually check
filter
, but I noticed it was part of a “delicate optics API”, so I got a little scared. Also, I guess you'd need an
every
in there?
Copy code
Lessons.items.every
  .filter { it.id == "1" }
  .name.set(lessons, "New Lesson One Name")
But thanks, this does indeed seem to be what I'm looking for! What's the reason for the
@DelicateOptic
annotation on it, though?
Oh, I guess it's this:
⚠️ Warning: when using
modify
with this optic, the transformation should not alter the values that are taken into account by the predicate. For example, it is fine to
filter
by
name
and then increase the
age
, but not to
filter
by
name
and then capitalize the
name
.
Okay, that's perfect, thanks a bunch!
a
What's the reason for the
@DelicateOptic
annotation on it, though?
exactly what you mention. I've been bitten a couple of times by this myself, so I think a remainder for "please read the docs and think twice" is good here.
s
I'm curious to know why or when it's a problem, though. All of these tests seem to yield the expected result:
Copy code
fun main() {
  val lessons = Lessons(listOf(Lesson("1", "Lesson One"), Lesson("2", "Lesson Two")))
  val filterId1 = Lessons.items.every.filter { it.id == "1" }

  // Overwriting the entire lesson
  println(filterId1.set(lessons, Lesson("3", "Lesson One")))
    // => Lessons(items=[Lesson(id=3, name=Lesson One), Lesson(id=2, name=Lesson Two)])

  // Setting the filtered value using `set`
  println(filterId1.id.set(lessons, "3"))
    // => Lessons(items=[Lesson(id=3, name=Lesson One), Lesson(id=2, name=Lesson Two)])

  // Setting the filtered value using `modify`
  println(filterId1.id.modify(lessons) { "3" })
    // => Lessons(items=[Lesson(id=3, name=Lesson One), Lesson(id=2, name=Lesson Two)])
}
Unless I'm misunderstanding the warning?
a
the problem here is that once you start chaining modifications, some of them may be "lost" since the filter no longer applies to them
s
I wasn't aware you could chain modifications, tbh. Aren't calls to
set
,
modify
or the like “terminal”? As in, they won't give you back a
Lens
,
Optional
,
Traversal
or anything Optics-specific?
a
I mean using several
modify
or
set
in a row
s
You mean something like this:
Copy code
var lessons = Lessons(listOf(Lesson("1", "Lesson One"), Lesson("2", "Lesson Two")))
val filterId1 = Lessons.items.every.filter { it.id == "1" }

lessons = filterId1.id.set("3")
lessons = filterId1.id.set("4")
I can definitely see why that wouldn't work, but perhaps in more contrived setups it's less obvious.