Skip to main content

Fan-out/Fan-in

Distribute work to N workers and collect all results in one place. A scatter-gather pattern used in parallel processing, map-reduce, and load distribution.

Hew

Workers hold a typed ActorType handle to the collector actor and call collector.report(). The actor reference carries the message route and the method call performs the send.

fan_out.hew
actor Collector {
    var total: i64 = 0;
 
    receive fn report(value: i64) {
        total += value;
    }
 
    receive fn get_total() -> i64 {
        total
    }
}
 
actor Worker {
    let collector: Collector;
 
    receive fn compute(n: i64) {
        collector.report(n * n).expect("actor stopped");
    }
}
 
fn main() {
    let collector = spawn Collector { total: 0 };
    let w1 = spawn Worker { collector: collector };
    let w2 = spawn Worker { collector: collector };
 
    scope {
        let first = fork w1.compute(3);
        let second = fork w2.compute(4);
        (await first).expect("actor stopped");
        (await second).expect("actor stopped");
    };
 
    match collector.get_total() {
        .Ok(total) => println(f"total: {total}"),
        .Err(_) => println("ask failed"),
    }
}

Go

Go uses two channels (jobs and results) with goroutines. The channel version needs an explicit lifecycle contract: close the jobs channel, drain the expected results, and add cancellation for early exits.

fan_out.go
func worker(id int, jobs <-chan int, results chan<- int) {
    for n := range jobs {
        results <- n * n
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)

    // Start workers
    for i := 0; i < 2; i++ {
        go worker(i, jobs, results)
    }

    // Send work
    jobs <- 3
    jobs <- 4
    close(jobs) // Forget this = deadlock

    // Collect results
    total := 0
    for i := 0; i < 2; i++ {
        total += <-results // Wrong count = goroutine leak
    }
    fmt.Printf("total: %d\n", total)
}

What this shows

Developer experience

Hew's actor-reference pattern eliminates channel lifecycle management. Workers don't know about channels — they call methods on their collector. The topology is expressed through actor fields, not channel wiring.

Debugging

Go channel pipelines can hang when close/drain/cancel paths are inconsistent. Hew's local actor version moves the main risks to message ordering, timeout choices, and actor lifecycle boundaries.

Trade-offs

Go's channel pattern supports dynamic topologies where the number of workers changes at runtime: channels can be passed around, combined, and multiplexed. Hew's example fixes the topology at spawn time and replaces channel close/drain coordination with actor lifecycle and request-error handling.