Skip to content

Composition

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 counter
b = 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 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 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 counter
proxy = 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 state

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

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", …]"]

Streams concatenate via the functor: stream-a (stream-b). Use this to extend an inbox:

# External commands first, then two bootstrap "inc" appended
bootstrapped-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; } ]
Contribute Community Sponsor