Skip to main content

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.

HewGoRustElixir/OTP
Concurrency modelTyped actors with structured task scopesGoroutines, channels, and shared-memory synchronizationOwnership plus library runtimes such as TokioIsolated BEAM processes with OTP behaviors
State isolationLocal actors own state; callers use typed message methodsShared memory, channels, or actor-like libraries by conventionOwnership prevents data races; shared state often uses Arc<Mutex<>>Processes isolate state and communicate by messages
Data-race safetyActor state is isolated and accessed through messagesRace detector catches many issues when tests exercise themCompile-time ownership and Send/Sync checksNo shared mutable state between BEAM processes
Fault recoverySupervisor syntax for local actorsLibraries or hand-rolled restart loopsLibraries/frameworks or hand-rolled restart loopsOTP supervisors are the reference model
Network typesWire types embedded in Hew source for Hew-to-Hew flowsProtobuf/JSON and ecosystem toolingserde, prost, and ecosystem toolingJSON, Protocol Buffers, or Erlang term formats by application choice
Runtime trade-offNative compiler and actor runtime; owned values clean up deterministically, and #[resource] types define one consuming close operationGC with goroutine schedulerNo GC; ownership/borrowing modelBEAM 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).

counter_service.hew
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.

counter_service.go
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.

counter_service.rs
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.