The asymmetry
After #245 (the unification work), the pipeline package keeps FanInStep while FanOutStep is gone — replaced by the Send sentinel. Visually that looks inconsistent: if FanIn exists, where's FanOut?
The honest answer is that FanInStep and Send were never symmetric to begin with, and the old FanOutStep name was misleading. The deletion just exposed it.
Why they aren't opposites
|
Layer |
Where it lives |
What it does |
FanInStep |
StepExecutor |
Runs at a node |
Merges multiple input ports into one output value via a Python merge_fn(list) -> Any. Basically lambda items: .... |
Send |
Control-flow sentinel |
Returned from a node |
Tells the engine to dispatch N parallel workers, each on a copy of state with the Send's payload merged in. |
You can't make Send a step. The act of "spawn N workers in parallel, each with its own state copy" can't be expressed as a single node's execute(context, inputs) -> Any — that's exactly why it has to be a sentinel the engine intercepts.
And the old FanOutStep didn't actually fan out either — it just returned a list. The parallelism (if any) was an emergent property of the downstream DAG topology, not something the step itself did. Renaming Send → FanOutStep would actually be a regression: it would hide the fact that Send is the thing that causes parallel execution, with per-worker state copies.
Why this matters
The current names invite a wrong mental model:
- A user sees
FanInStep and asks "where's FanOutStep?" — searches, finds it's gone, gets confused about whether the framework supports fan-out at all.
- A user finds
Send and FanInStep separately and doesn't realize they're meant to compose, because the names suggest different domains.
Options
Option A — Rename FanInStep → MergeStep (or JoinStep)
Kills the false symmetry. The name accurately describes what it does: merge N input ports at a node. No fan-in semantics are implied or required.
- Small PR —
steps.py rename + __init__.py re-export.
- Breaking unless we keep
FanInStep = MergeStep as a deprecated alias.
- Aligns with our principle that names should describe behavior, not topology buzzwords.
Option B — Leave FanInStep alone
FanInStep is rarely used in practice (most users write a CallableStep with a merge lambda inline). The cost of the visual inconsistency is low; the cost of a rename + deprecation cycle is real.
- No code change.
- Accept that visual symmetry was already broken — documenting it in
docs/pipeline.md is enough.
Option C — Add a Collect sentinel for true fan-in symmetry
Mirror Send with a Collect(targets) sentinel — "wait for these N nodes, then merge their state and pick the next step." This would give true symmetry: Send for parallel out, Collect for parallel in.
- Bigger change. Requires engine support for "park current node until N targets complete."
- Mostly redundant with what the topological scheduler already does for fan-in via DAG edges + reducers (Layer 3).
- I'd skip this unless a real use case shows up — it adds API surface for marginal value.
Recommendation
Option A if we're already touching steps.py for #239 (the BranchStep/FanOutStep migration cleanup). Bundle the rename into that PR.
Option B if steps.py stays untouched for now. The asymmetry is documented; users who hit it can read the docstring.
Option C is parked unless someone needs collect-style join semantics that the existing topology + reducers can't express.
Refs
The asymmetry
After #245 (the unification work), the pipeline package keeps
FanInStepwhileFanOutStepis gone — replaced by theSendsentinel. Visually that looks inconsistent: ifFanInexists, where'sFanOut?The honest answer is that
FanInStepandSendwere never symmetric to begin with, and the oldFanOutStepname was misleading. The deletion just exposed it.Why they aren't opposites
FanInStepmerge_fn(list) -> Any. Basicallylambda items: ....SendYou can't make
Senda step. The act of "spawn N workers in parallel, each with its own state copy" can't be expressed as a single node'sexecute(context, inputs) -> Any— that's exactly why it has to be a sentinel the engine intercepts.And the old
FanOutStepdidn't actually fan out either — it just returned a list. The parallelism (if any) was an emergent property of the downstream DAG topology, not something the step itself did. RenamingSend→FanOutStepwould actually be a regression: it would hide the fact thatSendis the thing that causes parallel execution, with per-worker state copies.Why this matters
The current names invite a wrong mental model:
FanInStepand asks "where'sFanOutStep?" — searches, finds it's gone, gets confused about whether the framework supports fan-out at all.SendandFanInStepseparately and doesn't realize they're meant to compose, because the names suggest different domains.Options
Option A — Rename
FanInStep→MergeStep(orJoinStep)Kills the false symmetry. The name accurately describes what it does: merge N input ports at a node. No fan-in semantics are implied or required.
steps.pyrename +__init__.pyre-export.FanInStep = MergeStepas a deprecated alias.Option B — Leave
FanInStepaloneFanInStepis rarely used in practice (most users write a CallableStep with a merge lambda inline). The cost of the visual inconsistency is low; the cost of a rename + deprecation cycle is real.docs/pipeline.mdis enough.Option C — Add a
Collectsentinel for true fan-in symmetryMirror
Sendwith aCollect(targets)sentinel — "wait for these N nodes, then merge their state and pick the next step." This would give true symmetry:Sendfor parallel out,Collectfor parallel in.Recommendation
Option A if we're already touching
steps.pyfor #239 (theBranchStep/FanOutStepmigration cleanup). Bundle the rename into that PR.Option B if
steps.pystays untouched for now. The asymmetry is documented; users who hit it can read the docstring.Option C is parked unless someone needs collect-style join semantics that the existing topology + reducers can't express.
Refs
BranchStep/FanOutStep— natural place to bundle Option A