Skip to main content

Removing Six Syntax Redundancies

· 5 min read
Building Hew: Part 7 of 16

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.

Syntax as of v0.5 — the pipe closure form; present before v0.6.0 cleanup.

Before:

let double = |x| x * 2;
let add = |a, b| a + b;
Syntax as of v0.6.0 design — the arrow form was specified as the winner at the time this post was written.

After (as designed for v0.6):

let double = (x) => x * 2;
let add = (a, b) => a + b;
v0.6 as shipped — the design reversed before release: the pipe form |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.

Syntax as of v0.5 — the race {} form; removed in v0.6.

Before:

race {
    fetch_from_a(),
    fetch_from_b(),
}
Syntax as of v0.6.0 design — the 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(),
}
v0.6 as shipped — the 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.

Syntax as of v0.5 — &&/|| were the original boolean operators before this design.

Before:

if connected && authenticated || is_admin {
    // ...
}
Syntax as of v0.6.0 design — 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 {
    // ...
}
v0.6 as shipped — the design reversed: &&/|| 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).

Syntax as of v0.5 — type-specific print functions; removed in v0.6.

Before:

print_i32(count);
print_str(name);
print_bool(flag);
v0.6+ — single unified 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.

Syntax as of v0.6.0 design — 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}"),
}
v0.6+ — current idiomatic await with an explicit actor and typed match.
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.

Syntax as of v0.5 — scope.spawn; renamed in v0.6.
scope.spawn(|| heavy_computation());
Syntax as of v0.6.0 design — scope.launch; consolidated into scope { fork { ... } } in the shipped release.

became:

scope.launch(|| heavy_computation());
v0.6+ — the 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();
}

Hew Notes

Language development, engineering articles, releases and community projects. About once a month, with an extra issue for a significant release. Written in English.

Subscribe to Hew Notes