The v0.5 spec had two lambda forms, two first-completion primitives, two sets of boolean operators, and two FFI keywords. Five revisions in four days, over 3,000 lines — and in every case the duplication was there because I added something without removing its predecessor. That’s what happens when you move fast: syntax piles up.
v0.6.0 added zero new features — every change was a removal, a rename, or a clarification.
Six Redundancies
The most visible cut was the pipe lambda syntax. Hew had inherited |x| expr from Rust alongside its own arrow form (x) => expr, and both compiled to exactly the same thing — two syntaxes for anonymous functions with nothing to show for it. The arrow form won because it groups parameters with parentheses and reads clearly at a glance.
Before:
let double = |x| x * 2;
let add = |a, b| a + b;After (as designed for v0.6):
let double = (x) => x * 2;
let add = (a, b) => a + b;|x| is what shipped in v0.6 and is the current syntax. The arrow form was removed.Current syntax (v0.6+):
fn apply(f: fn(i64) -> i64, x: i64) -> i64 { return f(x); }
fn main() {
let double = |x| x * 2;
print(apply(double, 5)); // 10
print(apply(|x| x + 1, 3)); // 4
}Same logic with race and select. Both waited for the first completed result from concurrent operations, but select offered a from keyword that bound the result to a name and allowed per-branch handling — including timeout clauses. Everything race could express, select could express more clearly.
race {} form; removed in v0.6.Before:
race {
fetch_from_a(),
fetch_from_b(),
}from a = expr select binding as designed; the syntax settled to v from expr form.After (as designed for v0.6):
select {
from a = fetch_from_a() => handle(a),
from b = fetch_from_b() => handle(b),
after 5s => timeout(),
}v from expr select binding is the current form.Current syntax (v0.6+):
actor A { receive fn fetch() -> string { return "a"; } }
actor B { receive fn fetch() -> string { return "b"; } }
fn handle(v: string) { print(v); }
fn timeout() { print("timed out"); }
fn main() {
let a = spawn A();
let b = spawn B();
select {
v from a.fetch() => handle(v),
v from b.fetch() => handle(v),
after 5s => timeout(),
}
}Boolean operators were a quieter change. The spec supported both &&/|| and and/or. I dropped the symbolic forms. (This is the kind of decision that feels obvious once you make it and agonizing before you do.) The English words scan faster in complex conditions, and there’s no ambiguity lost.
&&/|| were the original boolean operators before this design.Before:
if connected && authenticated || is_admin {
// ...
}and/or were specified as the winner at the time this post was written.After (as designed for v0.6):
if connected and authenticated or is_admin {
// ...
}&&/|| are the current boolean operators. The English word forms were removed.Current syntax (v0.6+):
fn main() {
let connected = true;
let authenticated = true;
let is_admin = false;
if connected && authenticated || is_admin {
print("authorised");
}
}Three smaller removals: foreign dropped in favour of extern — two spellings, same meaning. check_cancelled() became is_cancelled(), since boolean-returning functions should read as predicates. And the family of type-specific print functions — print_i32, print_str, print_bool — collapsed into a single println(dyn Display).
Before:
print_i32(count);
print_str(name);
print_bool(flag);println handles all types.After:
fn main() {
let count: i64 = 42;
let name: string = "alice";
let flag: bool = true;
println(count);
println(name);
println(flag);
}Three Dead Ends
The async fn keyword had been in the grammar since v0.2, carried over from early concurrency explorations. But Hew’s actors handle concurrency through message passing, and regular functions run within structured scope contexts. async fn suggested a distinction that didn’t exist. (I should have caught this two revisions ago.)
Top-level let and var bindings contradicted the no-global-mutable-state principle the actor model depends on. If any actor could write a global variable, the isolation guarantees that make message passing safe would collapse. Constants at module scope stayed — immutable data shared across actors is fine — but mutable bindings at the top level had to go.
The third was a rename. yield had been used for cooperative scheduler yielding — letting a long-running actor give up its time slice. But with generators coming, yield needed to mean what everyone expects it to mean. Renaming the scheduler operation to cooperate was more descriptive anyway.
Clarifications
The most important: await’s return type. Previous spec versions were vague about what happened when a task was cancelled while being awaited. v0.6.0 made it explicit — await returns Result<T, CancellationError>. Cancellation is a regular error, not a trap that crashes the actor.
server.fetch(url).await chained await form and Response type shown; current usage wraps actors explicitly.let result: Result<Response, CancellationError> = server.fetch(url).await;
match result {
Ok(resp) => process(resp),
Err(e) => log("cancelled: {e}"),
}actor Server {
receive fn fetch(url: string) -> string {
return "response";
}
}
fn process(resp: string) { print(resp); }
fn log_err(e: string) { print(e); }
fn main() {
let server = spawn Server();
let result = await server.fetch("http://example.com");
match result {
.Ok(resp) => process(resp),
.Err(_e) => log_err("cancelled"),
}
}scope.spawn was renamed to scope.launch to distinguish it from actor spawning — launching a task within a scope is fundamentally different from spawning an independent actor. Scopes were also clarified to use join semantics: a scope block doesn’t exit until every task launched within it has completed or been cancelled.
scope.spawn; renamed in v0.6.scope.spawn(|| heavy_computation());scope.launch; consolidated into scope { fork { ... } } in the shipped release.became:
scope.launch(|| heavy_computation());scope { fork { ... } } block syntax is what shipped.fn log_result() { print("done"); }
actor Worker {
receive fn run() {
scope {
fork { log_result() }
}
}
}
fn main() {
let w = spawn Worker();
w.run();
}