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].

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