Hewing Your First Distributed Service
In six steps you'll go from Hello World to a distributed key-value store — learning actors, supervision, and wire types along the way.
Step 1: Hello World
Every Hew program starts with a main function. Let's begin with the basics: bindings, types, and functions.
fn main() -> i32 { // Immutable binding let greeting: String = "Hello, Hew!"; println_str(greeting); // Mutable binding var count: i32 = 0; count = count + 1; // Other primitive types let pi: f64 = 3.14159; let active: bool = true; 0 // exit code}let creates an immutable binding; var creates a mutable one. Hew's core types include i32, f64, String, and bool.
Functions can take parameters and return values:
fn add(a: i32, b: i32) -> i32 { a + b}fn greet(name: String) -> String { "Hello, " + name + "!"}fn main() -> i32 { let sum = add(3, 4); println_int(sum); // 7 let msg = greet("world"); println_str(msg); // Hello, world! 0}Compile and run with hewc hello.hew && ./hello. Now let's move on to the feature that makes Hew special: actors.
Step 2: Actors & Messages
An actor is an isolated, single-threaded state machine with a mailbox. Actors never share mutable state — they communicate exclusively through messages. This gives you compile-time data-race freedom without locks.
Define an actor with the actor keyword. Message handlers are marked with receive fn; internal helpers use plain fn. Callers invoke handlers as direct method calls on the actor reference — no .send() or .ask() needed.
actor Counter { var count: i32 = 0; // Fire-and-forget: no return type, caller does not await receive fn increment() { self.count = self.count + 1; } // Fire-and-forget with a parameter receive fn add(n: i32) { self.count = self.count + n; } // Request-response: has return type, caller must await receive fn get_count() -> i32 { self.count }}fn main() -> i32 { // Spawn an actor let counter = spawn Counter { count: 0 }; // Fire-and-forget — direct method calls, no await needed counter.increment(); counter.increment(); counter.add(10); 0}spawn creates a new actor and returns an ActorRef. Fire-and-forget calls like counter.increment() enqueue a message and return immediately. Each actor processes one message at a time, so there are never data races on count.
Step 3: Request-Response Patterns
Fire-and-forget is great for commands, but sometimes you need a reply. A receive fn with a return type creates a request-response handler. Callers await the method call to get the result — no .ask() needed.
actor Calculator { var history: Vec<String> = Vec::new(); receive fn compute(op: String, a: f64, b: f64) -> Result<f64, String> { let result = if op == "add" { Ok(a + b) } else if op == "mul" { Ok(a * b) } else if op == "div" { if b == 0.0 { Err("division by zero") } else { Ok(a / b) } } else { Err("unknown operation") }; // Record in history if let Ok(val) = result { self.history.push(op + ": " + f64_to_string(val)); } result } receive fn get_history() -> Vec<String> { self.history.clone() }}fn main() -> i32 { let calc = spawn Calculator { history: Vec::new() }; // Direct method call; await blocks until the reply arrives let sum = await calc.compute("add", 10.0, 20.0); match sum { Ok(val) => println_str("Sum: " + f64_to_string(val)), Err(e) => println_str("Error: " + e), } // The ? operator propagates errors let product = await calc.compute("mul", 3.0, 7.0)?; println_str("Product: " + f64_to_string(product)); 0}Result<T, E> is Hew's standard error type. The ? operator propagates errors — if the result is Err, the function returns early. Combined with await and direct method calls, you get synchronous-style code with full concurrency under the hood.
Step 4: Supervision & Fault Tolerance
In production services, crashes are inevitable. Hew follows Erlang's "let it crash" philosophy: instead of defensive error handling everywhere, you define supervisors that automatically restart failed actors.
actor Worker { var id: i32 = 0; receive fn process(job: String) { println_str("Worker " + int_to_string(self.id) + " processing: " + job); // If this traps, the supervisor restarts us }}// Supervisor owns and monitors child actorssupervisor WorkerPool { strategy: one_for_one, max_restarts: 5, window: 60s, children { child w1: Worker restart(permanent); child w2: Worker restart(permanent); child w3: Worker restart(transient); }}Key concepts:
- one_for_one: only the crashed child is restarted — siblings keep running.
- permanent: always restart, regardless of exit reason.
- transient: restart only on abnormal exit (crashes), not on normal shutdown.
- temporary: never restart — once stopped, it's gone.
- max_restarts / window: if the supervisor exceeds 5 restarts in 60 seconds, the failure escalates to the parent supervisor.
Supervisors form a tree. Your top-level supervisor owns mid-level supervisors, which own workers. A crash in a leaf actor is contained and recovered automatically — your service stays up.
Step 5: Wire Types & Networking
Actors on the same machine communicate via in-process messages. But distributed services need to talk over the network. Hew's wire struct and wire enum give you network-ready serialization with built-in schema evolution.
// Every field has a numeric tag (@1, @2, ...) for stable serialization.// Tags must NEVER be reused — even after deleting a field.wire struct PutRequest { key: string @1; value: string @2; ttl: i32? @3; // optional field}wire struct GetRequest { key: string @1;}wire struct GetResponse { found: bool @1; value: string? @2;}wire enum RequestType { Get; Put; Delete;}Schema evolution rules:
- Never reuse a tag number — deleted fields should have their tags reserved.
- New fields must be optional (
?) or have defaults so old readers can still decode. - Encoding is automatic — Hew generates efficient binary serialization (HBF) from wire definitions.
- Use
hew wire check --against <schema>in CI to catch breaking changes.
Wire types implement the Send trait, so they can be used directly as actor messages — both locally and across the network.
Step 6: Building a Distributed Key-Value Store
Let's tie everything together. We'll build a key-value store with a frontend actor that accepts client requests and a backend actor that stores data.
Message protocol
// Wire types for network transportwire struct PutMsg { key: string @1; value: string @2;}wire struct GetMsg { key: string @1;}wire struct GetResult { found: bool @1; value: string? @2;}Backend actor
actor KVBackend { var store: HashMap<String, String> = HashMap::new(); receive fn put(msg: PutMsg) { self.store.insert(msg.key, msg.value); } receive fn get(msg: GetMsg) -> GetResult { match self.store.get(msg.key) { Some(val) => GetResult { found: true, value: Some(val.clone()) }, None => GetResult { found: false, value: None }, } } receive fn count() -> i32 { self.store.len() }}Frontend actor
actor KVFrontend { var backend: ActorRef<KVBackend>; receive fn handle_put(msg: PutMsg) { // Fire-and-forget to backend self.backend.put(msg); } receive fn handle_get(msg: GetMsg) -> GetResult { // Request-response to backend let result = await self.backend.get(msg); result }}Supervision tree
supervisor KVSupervisor { strategy: one_for_one, max_restarts: 10, window: 60s, children { child backend: KVBackend restart(permanent); child frontend: KVFrontend restart(permanent); }}Putting it all together
fn main() -> i32 { // Start the supervised service let sup = spawn KVSupervisor; // Get references to child actors let frontend = sup.child_ref("frontend"); // Store some data — direct method calls, fire-and-forget frontend.handle_put(PutMsg { key: "lang", value: "hew" }); frontend.handle_put(PutMsg { key: "version", value: "0.5" }); // Retrieve data — direct method call, await for response let result = await frontend.handle_get(GetMsg { key: "lang" }); match result.value { Some(val) => println_str("Found: " + val), None => println_str("Not found"), } // Use select for timeout on slow operations let guarded = select { value from frontend.handle_get(GetMsg { key: "version" }) => value, after 2.seconds => GetResult { found: false, value: None }, }; 0}You now have a complete distributed service: wire types define the protocol, actors handle concurrency without locks, and a supervisor keeps everything running through failures. Named actors use direct method calls; lambda actors use the <- operator. The select expression lets you race concurrent operations or apply timeouts. From here, you can add more backends for sharding, plug in network listeners for remote clients, or grow the supervision tree as your service scales.
Next steps
- Actor Model — deep dive into message passing, lambda actors, and structured concurrency
- Type System — generics, capabilities, and pattern matching
- Standard Library — built-in types and IO