Search code examples
macrosrustrust-macros

How to implement the Lispian cond macro?


Intended usage:

cond! {
  x > 5 => 0,
  x < 3 => 1,
  true => -1
}

Should expand to:

if x > 5 { 0 } else if x < 3 { 1 } else if true { -1 }

Note that it doesn't produce a catch-all else { ... } suffix.

My attempt:

macro_rules! cond(
    ($pred:expr => $body:expr) => {{
        if $pred {$body}
    }};
    ($pred:expr => $body:expr, $($preds:expr => $bodies:expr),+) => {{
        cond! { $pred => $body } else cond! { $($preds => $bodies),+ }
    }};
);

However, the compiler complains about the else keyword.

error: expected expression, found keyword `else`
  --> src/main.rs:32:34
   |
32 |           cond! { $pred => $body } else cond! { $($preds => $bodies),+ }
   |                                    ^^^^

Solution

  • Macros in Rust don't perform textual substitution like the C preprocessor does. Moreover, the result of a macro is already "parsed", so you can't just append something after the macro invocation that's supposed to be part of what the macro expands to.

    In your case, you can't put an else after the first cond! invocation because the compiler has already finished parsing the if expression; you need to put the if and the else together. Likewise, when you invoke cond! again after the else, you need to add braces around the call, because the sequence else if does not begin a nested if expression.

    macro_rules! cond {
        ($pred:expr => $body:expr) => {
            if $pred { $body }
        };
        ($pred:expr => $body:expr, $($preds:expr => $bodies:expr),+) => {
            if $pred { $body } else { cond! { $($preds => $bodies),+ } }
        };
    }
    

    Ultimately though, this macro is pretty much useless. An if expression without an else clause always has its type inferred to be (), so unless all the branches evaluate to () (or diverge), the expanded macro will produce type mismatch errors.