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.

Before:

let double = |x| x * 2;
let add = |a, b| a + b;

After:

let double = (x) => x * 2;
let add = (a, b) => a + b;

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.

Before:

race {
    fetch_from_a(),
    fetch_from_b(),
}

After:

select {
    from a = fetch_from_a() => handle(a),
    from b = fetch_from_b() => handle(b),
    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.

Before:

if connected && authenticated || is_admin {
    // ...
}

After:

if connected and authenticated or is_admin {
    // ...
}

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);

After:

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.

let result: Result<Response, CancellationError> = server.fetch(url).await;
match result {
    Ok(resp) => process(resp),
    Err(e) => log("cancelled: {e}"),
}

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(|| heavy_computation());

became:

scope.launch(|| heavy_computation());