Skip to content

Ping-Pong

Two actors in a lazy let: the second actor’s inbox depends on the first actor’s outbox. Nix laziness resolves the order.

flowchart LR
  ext["external inbox\n["inc","inc","inc"]"]
  pong["pong\ncounter-c"]
  xf["st.map\n_ → "get""]
  ping["ping\ncounter-c\n(fresh — always 0)"]
  out["outbox\n[0,0,0]"]

  ext --> pong
  pong -->|"outbox [1,2,3]"| xf
  xf -->|"["get","get","get"]"| ping
  ping --> out
ping-pong-c =
{ inbox }:
let
pong = counter-c { inherit inbox; };
ping = counter-c { inbox = pong.outbox (st.map (_: "get")); };
in
{ outbox = ping.outbox; };
result = ping-pong-c { inbox = st "inc" "inc" "inc"; };
result.outbox.toList
# [ { right = 0; } { right = 0; } { right = 0; } ]

pong processes the external seed inbox and produces 3 replies. st.map (_: "get") transforms each reply — discarding the value — into the string "get". ping receives those three "get" messages. Since ping is a fresh counter starting at 0 and "get" never increments, it returns { right = 0 } each time.

No circularity: ping depends on pong; pong does not depend on ping.

Extend the lazy-let pattern to N stages:

flowchart LR
  i["["inc"×3]"]
  a["stage-a\ncounter-c"]
  xfa["→"inc""]
  b["stage-b\ncounter-c"]
  xfb["→"get""]
  c["stage-c\ncounter-c"]
  out["[0,0,0]"]

  i --> a -->|"[1,2,3]"| xfa --> b -->|"[1,2,3]"| xfb --> c --> out
stage-a = counter-c { inbox = st "inc" "inc" "inc"; };
stage-b = counter-c { inbox = stage-a.outbox (st.map (_: "inc")); };
stage-c = counter-c { inbox = stage-b.outbox (st.map (_: "get")); };
stage-c.outbox.toList
# [ { right = 0; } { right = 0; } { right = 0; } ]
  • stage-a gets ["inc","inc","inc"] → outbox [{right=1},{right=2},{right=3}]
  • stage-b gets ["inc","inc","inc"] (each stage-a reply mapped to "inc") → outbox [{right=1},{right=2},{right=3}]
  • stage-c gets ["get","get","get"] (each stage-b reply mapped to "get") — counter starts at 0, "get" never increments → [{right=0},{right=0},{right=0}]

The key insight: a dependency graph is fine as long as it is acyclic. Lazy let in Nix allows forward references — you can reference pong.outbox before pong is evaluated, because Nix only evaluates pong when its value is needed. Since pong does not depend on ping, there is a valid evaluation order.

A true cycle (actor A’s outbox feeds A’s own inbox, producing more output) would diverge. dnzl’s design avoids this: feedback loops must terminate (see Lazy Evaluation).

Contribute Community Sponsor