Handlers
An ActionHandler[M, V, E] is a partial function from action to IO[E, ActionResult[M, E]],
scoped to slice V. Inside handle(lens) { ... } the lens is ambient: update, updated,
and noChange read and write that slice.
update, updated, noChange
noChange produces a clean ActionResult. Dispatch-level FastEq then skips every listener.
for
c <- Conduit(State(0, "x"))(countHandler)
_ <- c(Op.Inc, Op.Inc, Op.Dec, Op.TouchOnly)
_ <- c.run()
s <- c.currentModel
yield s.count1Live: focus and follow-ups
Inc / Dec / Reset move count. Set label writes label through focus. Inc twice
increments once in the handler and returns Inc as ActionResult.next, so one click becomes
two dispatches.
Mermoid.diagram(composeDiag)for
(_, ctx) <- DocsRuntime.live(State(0, "x"))(focused)
count <- ctx.squawk(_.count)
label <- ctx.squawk(_.label)
yield E.div(
E.p("count: ", E.strong(count.map(_.toString)), " label: ", E.strong(label)),
E.button(Events.onClick(_ => ctx(Op.Dec)), "−"),
E.button(Events.onClick(_ => ctx(Op.Inc)), "+"),
E.button(Events.onClick(_ => ctx(Op.IncTwice)), "Inc twice"),
E.button(Events.onClick(_ => ctx(Op.ResetCount)), "Reset"),
E.button(Events.onClick(_ => ctx(Op.SetLabel("on"))), "Label on"),
E.button(Events.onClick(_ => ctx(Op.SetLabel("off"))), "Label off"),
)count: 0 label: x
for
c <- Conduit(State(0, "x"))(focused)
_ <- c(Op.IncTwice)
_ <- c.run()
s <- c.currentModel
yield s.count2>> vs ++
>> (orElse) is first-match composition: split handlers by slice, try the left one, then
the right. ++ (fold) runs both when they match the same action. The second sees the
first's model; next concatenates; dirty is OR so update ++ noChange still notifies.
Mermoid.diagramInteractive(composeDiag, initialWidth = 560)Unhandled and errors
The default unhandled path is a defect. onUnhandled maps the action to a typed E.
widen[E2] is for composing a Nothing handler with one that can fail. effectOnly runs a
side effect and returns a clean result; it always sees the whole model, even inside focus.