Collection lenses
The Optics macro cannot derive xs(i) or m(k): those need a runtime value.
CollectionLens adds at / atVector / key, each focusing Option[V].
Some(v)in range updates;Some(v)atindex == sizeappendsNonein range removes; out of range is a no-op
key on Map
import conduit.CollectionLens.*
val milk = Optics[Board](_.todos).key("milk")
milk.get(board) // Option[Todo]
milk.set(board, None) // remove
milk.set(board, Some(t)) // upsert
Mermoid.diagram(coll){
val milk = Optics[Board](_.todos).key("milk")
val start = Board(Map("milk" -> Todo("buy milk", false)))
val done = milk.get(start).map(t => milk.set(start, Some(t.copy(done = true)))).getOrElse(start)
val gone = milk.set(done, None)
(milk.get(start).exists(!_.done), milk.get(done).exists(_.done), milk.get(gone).isEmpty)
}(true,true,true)Live todos
Add, toggle, and remove go through key. The list is a Map[String, Todo] so each row is
element-scoped: toggling one id does not notify a squawkKey on a sibling.
for
(_, ctx) <- DocsRuntime.live(Board(Map("a" -> Todo("write tests", false))))(handler)
todos <- ctx.squawk(_.todos)
yield E.div(
E.button(Events.onClick(_ => ctx(BoardOp.Add("b", "ship docs"))), "Add 'ship docs'"),
E.button(Events.onClick(_ => ctx(BoardOp.Toggle("a"))), "Toggle first"),
E.button(Events.onClick(_ => ctx(BoardOp.Remove("a"))), "Remove first"),
E.pre(
todos.map { m =>
if m.isEmpty then "(empty)"
else
m.toList
.sortBy(_._1)
.map { case (id, t) =>
s"$id: ${t.text} ${if t.done then "[done]" else "[todo]"}"
}
.mkString("\n")
}
),
)a: write tests [todo]
at / atVector
Same Option protocol on List and Vector. at(length) with Some(v) appends, which is
how a lawful "write past the end" still satisfies set-get at that index.
Mermoid.diagramInteractive(coll, initialWidth = 560)viewport 560px