Composition
Outbox → inbox wiring
Section titled “Outbox → inbox wiring”Wire one actor’s outbox as the next actor’s inbox. Nix laziness resolves evaluation order.
a = counter-c { inbox = st "inc" "inc" "inc"; };# a.outbox = ST [{ right = 1 } { right = 2 } { right = 3 }]
# Map each reply to "inc" and feed to the next counterb = counter-c { inbox = a.outbox (st.map (_: "inc")); };b.outbox.toList# → [ { right = 1; } { right = 2; } { right = 3; } ]a.outbox is a ST. Calling it with a combinator transforms it in place.
flowchart LR
in["["inc","inc","inc"]"]
a["counter-c A"]
b["counter-c B"]
out["[{right=1},{right=2},{right=3}]"]
in --> a -->|"outbox\n(st.map →"inc")"| b --> out
merge — fan-in
Section titled “merge — fan-in”merge concatenates a list of streams sequentially:
merge [ (st "a" "b") (st "c" "d") ]# → ST ["a" "b" "c" "d"]Feed multiple upstream outboxes into one actor:
cmds-a = st "inc" "inc";cmds-b = st "inc" "get";
shared = counter-c { inbox = merge [ cmds-a cmds-b ]; };shared.outbox.toList# → [ { right = 1; } { right = 2; } { right = 3; } { right = 3; } ]flowchart LR a["cmds-a\n["inc","inc"]"] b["cmds-b\n["inc","get"]"] m["merge"] c["counter-c"] out["[1,2,3,3]"] a --> m b --> m m -->|"["inc","inc","inc","get"]"| c --> out
send — forward to a ref
Section titled “send — forward to a ref”send ref msgs calls ref { inbox = msgs } and returns { reply = ref_outbox }. Each call creates a fresh actor session.
# Each message is routed to a fresh counterproxy = msg: send counter-c (st msg);proxy-c = actor proxy;
(proxy-c { inbox = st "inc" "inc" "get"; }).outbox.toList# → [ { right = 1; } { right = 1; } { right = 0; } ]# Each "inc" and "get" goes to its own fresh counter — no shared statewhen-c — content-based routing
Section titled “when-c — content-based routing”when-c pred inbox f filters inbox to messages where pred holds, then maps with f:
inbox = st { type = "count"; cmd = "inc"; } { type = "div"; dividend = 10; divisor = 2; } { type = "count"; cmd = "inc"; } { type = "count"; cmd = "get"; };
counts = counter-c { inbox = when-c (m: m.type == "count") inbox (m: m.cmd); };divides = div-c { inbox = when-c (m: m.type == "div") inbox (m: m); };Each actor sees only its slice. The shared inbox is never mutated.
flowchart TB inbox["shared inbox"] wc1["when-c type==count → cmd"] wc2["when-c type==div → msg"] c["counter-c"] d["div-c"] inbox --> wc1 --> c inbox --> wc2 --> d
Multi-output cycles
Section titled “Multi-output cycles”A cycle-c can return any attrset — not just { outbox }:
validate-c = { inbox }: let a = validator-c { inherit inbox; }; in { values = a.outbox.right; errors = a.outbox.left; };
result = validate-c { inbox = st 5 (-1) 3 0 7; };result.values.toList# → [ 5 3 7 ]result.errors.toList# → [ "non-positive: -1" "non-positive: 0" ]flowchart LR inbox["inbox [5,-1,3,0,7]"] v["validator-c"] inbox --> v v -->|"outbox.right → values"| vals["values [5, 3, 7]"] v -->|"outbox.left → errors"| errs["errors ["non-positive: -1", …]"]
Inbox concat
Section titled “Inbox concat”Streams concatenate via the functor: stream-a (stream-b). Use this to extend an inbox:
# External commands first, then two bootstrap "inc" appendedbootstrapped-c = { inbox }: counter-c { inbox = merge [ inbox (st "inc" "inc") ]; };
result = bootstrapped-c { inbox = st "inc"; };result.outbox.toList# → [ { right = 1; } { right = 2; } { right = 3; } ]