How Hew Compares
These pages put small, hand-written versions of the same concurrency and service patterns side by side so you can compare API shape, control flow, and lifecycle choices.
Pattern deep-dives
Each page compares a specific concurrency or systems pattern across languages with full code examples and analysis.
Select & Join
Race async operations or wait for all of them. Hew vs Go vs Rust.
Actor-call races and joinsFan-out/Fan-in
Distribute work to N workers, collect results. Hew vs Go.
Local scatter-gather topologyBackpressure
Bounded mailboxes and overflow strategies. Hew vs Go.
Mailbox capacity and overflowSupervision
Crash detection, restart budgets, and strategies. Hew vs Erlang/OTP, Elixir/OTP, and Go.
Local supervision policyWire Types
Evolvable network message formats. Hew vs Go+Protobuf vs Rust+serde.
Hew-to-Hew wire schemasActor Pipeline
Multi-stage processing chains with typed actor references. Hew vs Go.
Typed local-actor stages| Hew | Go | Rust | Elixir/OTP | |
|---|---|---|---|---|
| Concurrency model | Typed actors with structured task scopes | Goroutines, channels, and shared-memory synchronization | Ownership plus library runtimes such as Tokio | Isolated BEAM processes with OTP behaviors |
| State isolation | Local actors own state; callers use typed message methods | Shared memory, channels, or actor-like libraries by convention | Ownership prevents data races; shared state often uses Arc<Mutex<>> | Processes isolate state and communicate by messages |
| Data-race safety | Actor state is isolated and accessed through messages | Race detector catches many issues when tests exercise them | Compile-time ownership and Send/Sync checks | No shared mutable state between BEAM processes |
| Fault recovery | Supervisor syntax for local actors | Libraries or hand-rolled restart loops | Libraries/frameworks or hand-rolled restart loops | OTP supervisors are the reference model |
| Network types | Wire types embedded in Hew source for Hew-to-Hew flows | Protobuf/JSON and ecosystem tooling | serde, prost, and ecosystem tooling | JSON, Protocol Buffers, or Erlang term formats by application choice |
| Runtime trade-off | Native compiler and actor runtime; owned values clean up deterministically, and #[resource] types define one consuming close operation | GC with goroutine scheduler | No GC; ownership/borrowing model | BEAM scheduling with per-process garbage collection |
Hew
Actors, supervision, and wire types are language constructs. In supported local actor paths, the Counter actor owns its state and callers use direct message-method calls like counter.increment(10).
actor Counter {
var count: i64 = 0;
receive fn increment(amount: i64) {
count = count + amount;
}
receive fn get_count() -> i64 {
count
}
}
supervisor CounterPool {
strategy: one_for_one;
intensity: 5 within 60s;
child counter: Counter { count: 0 } restart: permanent;
}
#[wire]
type CounterUpdate {
counter_id: u32 @1;
new_count: i64 @2;
timestamp: u64 @3;
}
fn main() {
let counter = spawn Counter { count: 0 };
counter.increment(10).expect("actor stopped");
match counter.get_count() {
.Ok(count) => println(count),
.Err(_) => println("ask failed"),
}
}Go
Go uses goroutines and channels for concurrency, while shared mutable state usually requires explicit synchronization such as sync.Mutex. Supervision and network schema choices are handled with libraries, conventions, or application code.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count int32
}
func NewCounter() *Counter {
return &Counter{}
}
func (c *Counter) Increment(amount int32) int32 {
c.mu.Lock()
defer c.mu.Unlock()
c.count += amount
return c.count
}
func (c *Counter) GetCount() int32 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Example restart logic; production code would add context cancellation
// and time-windowed budgets or use a supervision library.
type CounterPool struct {
counters []*Counter
maxRestarts int
restartCount int
}
func NewCounterPool(size int) *CounterPool {
pool := &CounterPool{
counters: make([]*Counter, size),
maxRestarts: 5,
}
for i := range pool.counters {
pool.counters[i] = NewCounter()
}
return pool
}
func (p *CounterPool) RestartChild(idx int) {
p.restartCount++
if p.restartCount > p.maxRestarts {
fmt.Println("Exceeded restart limit")
return
}
p.counters[idx] = NewCounter()
}
// One possible schema choice: JSON tags on a plain struct.
type CounterUpdate struct {
CounterID uint32 `json:"counter_id"`
NewCount int32 `json:"new_count"`
Timestamp uint64 `json:"timestamp"`
}
func main() {
counter := NewCounter()
counter.Increment(10)
fmt.Printf("count: %d\n", counter.GetCount())
}Rust
Rust prevents data races at compile time through ownership and borrowing. Shared state across threads often uses Arc<Mutex<>> wrappers, while supervision and wire formats are typically provided by crates or application frameworks.
use std::sync::{Arc, Mutex};
struct Counter {
count: i32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn increment(&mut self, amount: i32) -> i32 {
self.count += amount;
self.count
}
fn get_count(&self) -> i32 {
self.count
}
}
type SharedCounter = Arc<Mutex<Counter>>;
fn new_shared_counter() -> SharedCounter {
Arc::new(Mutex::new(Counter::new()))
}
fn shared_increment(counter: &SharedCounter, amount: i32) -> i32 {
let mut c = counter.lock().unwrap();
c.increment(amount)
}
// Example restart logic; production code would likely use a runtime
// or framework-specific supervision pattern.
struct CounterPool {
counters: Vec<SharedCounter>,
max_restarts: u32,
restart_count: u32,
}
impl CounterPool {
fn new(size: usize) -> Self {
let counters = (0..size).map(|_| new_shared_counter()).collect();
CounterPool {
counters,
max_restarts: 5,
restart_count: 0,
}
}
fn restart_child(&mut self, idx: usize) {
self.restart_count += 1;
if self.restart_count > self.max_restarts {
eprintln!("Exceeded restart limit");
return;
}
self.counters[idx] = new_shared_counter();
}
}
// One possible schema choice: serde derives for JSON.
// #[derive(Serialize, Deserialize)]
struct CounterUpdate {
counter_id: u32,
new_count: i32,
timestamp: u64,
}
fn main() {
let counter = new_shared_counter();
let count = shared_increment(&counter, 10);
println!("count: {}", count);
}What this shows
State isolation & messaging
In Hew, actor is a language construct for the supported local-runtime path. Named actors use message-method calls such as counter.increment(10); lambda actor handles use completion calls such as worker(42). Go, Rust, and Elixir/OTP all have strong answers here; they place the trade-off in different parts of the language/runtime/tooling stack.
Fault recovery
Hew's supervisor declaration makes restart policy visible in Hew source. Go and Rust usually express equivalent behavior through application code or libraries; Elixir/OTP and Erlang/OTP provide supervision as a core runtime abstraction.
Network types
Hew's #[wire] type defines tagged message formats directly in the language for Hew-to-Hew flows. Protocol Buffers supplies a language-neutral schema and generated bindings; serde integrates Rust data types with a broad range of formats.
Trade-offs
Choose the model that matches the service boundary: Hew integrates actors, supervision, and Hew-to-Hew wire types in the language; Go emphasizes goroutines and channels; Rust combines ownership with library-selected runtimes; Elixir/OTP builds services around BEAM processes and OTP behaviors.