Skip to content

Design idea: patterns as control flow conditions #5101

@geoffromer

Description

@geoffromer

Languages that support pattern matching often have ways of combining it with if/else control flow, and we've repeatedly discussed adding such features to Carbon, but don't yet have an accepted design for them.

if let

The most prominent such feature is using a pattern as the condition of an if statement, such as if let in Rust, and it has repeatedly come up as a desirable feature for Carbon.

One notable design question here is what the syntax should be. The most obvious options I see are:
A. if let (x: i32, y) = Foo() { ... }
B. if (let (x: i32, y) = Foo()) { ... }
C. if let ((x: i32, y) = Foo()) { ... }

Option A is closest to Rust's syntax, and is arguably the most readable, but it risks ambiguity about where the body block begins, because { can also occur within the initializer expression. Even if it's not formally ambiguous, it raises many of the same problems as optional semicolons (which we rejected in p2665).

Option B consists of allowing a let declaration take the place of the condition expression, which is both an advantage and a disadvantage: it's superficially intuitive and easy to remember, but that appearance may be misleading -- it suggests that if and let are orthogonal language constructs that are being composed in this code, which isn't at all the case.

Option C is in some ways the inverse of option B: it violates syntactic expectations about let, so readers are less likely to mistakenly think they understand it, but more likely to be confused by it.

The proposal should also address whether this syntax works with else if and while, whether it works with var in place of let, and whether it works with expression-if (my recommendation would be "yes", "yes", and "no", respectively).

let ... else

Some languages support another form of conditional pattern matching, where bindings in the pattern are added to the enclosing scope (just like with unconditional let) instead of starting a new scope, and an attached else block contains the code to execute if the pattern does not match. To prevent those bindings from being accessed when the pattern does not match, the code must ensure (and typically the compiler enforces) that the end of the else block is unreachable, for example by having it unconditionally return, break, or terminate the program. Rust's let ... else and Swift's guard ... else are examples of this kind of construct.

The early-exit requirement makes this feature more complex than if let, but it allows the "happy path" (where all matches succeed) to be written without excessive nesting, which makes it a good fit for error handling.

The precedents of Swift and Rust point toward a syntax like guard let (x: i32, y) = Foo() else { ... }, but as with if let, there is a risk of ambiguity or near-ambiguity, because the else { tokens that mark the end of the condition can also occur within it, as part of the initializer expression.

A proposal for this feature would also need to address how (if at all) the compiler enforces the requirement that control not reach the end of the else block, which may be a tricky balance between expressivity and implementation complexity. It may also rely on other not-yet-designed language features, such as an empty/no-return type.

In light of that additional complexity, it may make sense to defer this feature to a separate proposal after if let, but we should at least keep this feature in mind when we're solving the syntax problem for if let.

Metadata

Metadata

Assignees

No one assigned

    Labels

    design ideaAn issue recording a specific language design idea that folks can potentially pick up.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions