Else Before If In Rust

louy2

Yufan Lou

Posted on April 27, 2020

Else Before If In Rust

Jonathan Boccara has got another new idea in his Else Before If. I am honestly amazed at all the surprising thoughts he comes up with and how he bends C++ to make them work. He cares about things most don't.

In this case, the idea is about control flow. We have always dealt with edge cases with if statements, and it has always been edge cases first, general cases last. Let me quote his example:

if (edgeCase1)
{
    // deal with edge case 1
}
else if (edgeCase2)
{
    // deal with edge case 2
}
else
{
    // this is the main case
}

I want to note in addition that this is not only C++, but universal. We have always followed the convention that conditions are matched from top to bottom, be it cond in Lisp, match in ML, case in Ruby, etc.

This, despite that we usually come up with the normal case first, and the edge cases after. Despite that we understand the normal case first, and then we can understand the edge cases.

This is a similar problem to error handling. Errors, after all, are exceptional edge cases. For Java and JavaScript programmers, try ... catch ... is their very familiar friends.

try {
    // this is the main case
} catch (err) {
    // deal with edge case 1
} catch (edgeCase2) {
    // deal with edge case 1
}

Being designed specifically for error handling, try ... catch ... has other skills up its sleeves, but syntax-wise it is the same idea.

Jonathan proposes the following syntax:

normally
{
    // this is the main case
}
unless (edgeCase1)
{
    // deal with edge case 1
}
unless (edgeCase2)
{
    // deal with edge case 2
}

Without modifying the parser, there are some compromises to make. Instead of blocks, each case is instead a closure, and normally is a template function that encloses the unless blocks rather than leading them. I'll leave the end result and the detail of the template magic to the original post.

Short story, I quickly implemented the idea in Rust, and have since gained appreciation of the separation of macro and generic programming.

Applying Jonathan's strategy, representing the branches as closures, quickly bumps into a large obstacle. The way Jonathan uses the syntax is very C++: declare storage, put in value, display the value at storage:

std::string text;

normally
{
    text = "normal case";
}
unless (edgeCase1)
{
    text = "edge case 1";
}
unless (edgeCase2)
{
    text = "edge case 2";
}

std::cout << textToDisplay << '\n';

That means the storage text is referred to, and therefore borrowed in, all three branches. In addition, text is mutated in each branch. C++ doesn't care much, but Rust has a stand against shared mutation. It is a Rust compiler error for both the normal branch and the unless branches to refer to and mutate x.

Why? Because the Rust compiler doesn't know that only one of the branches will be executed. Unfortunately, we cannot tell it either, because Rust neither has the syntax for nor is capable of analyzing general control flow constraints like this. It is not a proof assistant. This is a trade-off among security, freedom of expression, and complexity of management.

On the other hand, the proposed syntax change is only a reordering of blocks. The change is not semantic, but purely syntactical. It is not some complicated data structure (like BTree) requiring verification for safety. Rust has a better tool for syntactic changes: macro.

macro_rules! normally {
    ( $norm_br:block $(unless ($cond:expr) $unle_br:block)* ) => {
        if (false) {}
        $(else if $cond $unle_br)*
        else $norm_br
    };
}

Playground

You can use the macro like this:

normally! {
    {
        x = "normal case".to_string();
    }
    unless (n == 10) {
        x = "unless case 1".to_string();
    }
    unless (n == 11) {
        x = "unless case 2".to_string();
    }
}

With the $norm_br:block $(unless ($cond:expr) $unle_br:block)* pattern, the macro matches $norm_br to the first block expression:

{
    x = "normal case".to_string();
}

$(unless ($cond:expr) $unle_br:block)* is a pattern group, with the * in the end indicating that we are looking for zero to many of it. Within the group, we look for unless keyword, after which is a pair of parenthesis enclosing an expression $cond representing the edge case, and the block expression to execute for that edge case.

So the macro sees unless and matches $cond to n == 10 and matches unle_br to:

{
    x = "unless case 1".to_string();
}

The macro does the same for the second unless block, and puts the matches into the example. Notably, $(else if $cond $unle_br)* repeats as many times as there are unless matches.

In the end the macro turns the normally! expression into this:

if (false) {}
else if n == 10 {
    x = "unless case 1".to_string();
}
else if n == 11 {
    x = "unless case 2".to_string();
}
else {
    x = "normal case".to_string();
}

I certainly feel this is simpler than the C++ template magic. C++ template feels like a compromise between a generic type system and a macro system. The mixture of concern really shows in its complexity in my opinion.


  1. Closure, lambda, they're the same thing.

  2. macro_rules! is the simpler way to write a macro in Rust, also called macro by example. The harder and more powerful way is procedural macro (proc_macro).

  3. unless is used in Ruby and Lisp to mean "if not", as in "Unless the egg is cooked, don't turn off the stove." It is also natural for "unless" in English to reject the sentence before, as in "Break two eggs, unless you don't have eggs, then pour 200cc of liquid egg." Natural language 🤷‍♂️.


If you feel that you've got something from this post, I am glad! Please don't hesitate to comment or reach out.

If you feel that you've got enough that you'd like to donate, please donate to Wikimedia Foundation, to which I owe infinitely.

💖 💪 🙅 🚩
louy2
Yufan Lou

Posted on April 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Else Before If In Rust
cpp Else Before If In Rust

April 27, 2020