Skip to main content
  1. About
  2. For Teams
Asked
Viewed 13k times
57

Here's Rust's assert_eq! macro implementation. I've copied only the first branch for brevity:

macro_rules! assert_eq {
    ($left:expr, $right:expr) => ({
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, left_val, right_val)
                }
            }
        }
    });
}

What's the purpose of the match here? Why isn't checking for non-equality enough?

2
  • 2
    Looks like it's evaluating the expressions.
    erip
    –  erip
    2018-02-11 14:06:02 +00:00
    Commented Feb 11, 2018 at 14:06
  • 5
    The principal motivator seems to be to extend the lifetime of temporary values created in the match statement to make assert_eq! more useful, see this commit: github.com/rust-lang/rust/commit/d3c831ba4a4 - I'm not answering since I can't explain why this works (yet). See this playground link for experiments: play.rust-lang.org/…
    udoprog
    –  udoprog
    2018-02-11 14:33:56 +00:00
    Commented Feb 11, 2018 at 14:33

2 Answers 2

67

Alright, let's remove the match.

    macro_rules! assert_eq_2 {
        ($left:expr, $right:expr) => ({
            if !($left == $right) {
                panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, $left, $right)
            }
        });
    }

Now, let's pick a completely random example...

fn really_complex_fn() -> i32 {
    // Hit the disk, send some network requests,
    // and mine some bitcoin, then...
    return 1;
}

assert_eq_2!(really_complex_fn(), 1);

This would expand to...

{
    if !(really_complex_fn() == 1) {
        panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, really_complex_fn(), 1)
    }
}

As you can see, we're calling the function twice. That's less than ideal, even more so if the result of the function could change each time it's called.

The match is just a quick, easy way to evaluate both "arguments" to the macro exactly once and bind them to variable names.

Sign up to request clarification or add additional context in comments.

5 Comments

Wouldn't this be possible with something like let left = $left;?
@JeroenBollen There is a slight difference between that and a match which crops up in edge cases, but I don't recall the details. I think it has something to do with the exact lifetime of the temporaries. Since I don't know, I avoided mentioning it.
Indeed, match expr { x => {...} } is the Rust equivalent of let x = expr in ..., in that it's the most relaxed with respect to lifetime temporaries and "should always work", e.g. match e { x => f(x) } should be always equivalent to f(e). On the other hand, let x = expr; is more "imperative" and doesn't allow keeping nested temporaries around, e.g. let x = f(&g()); only compiles if f uses its reference argument without returning it, as the temporary it points to only lives for the duration of the call.
I couldn't find it before, but this is one of the blog posts about lifetimes of temporaries (note that it might not entirely reflect current Rust): smallcultfollowing.com/babysteps/blog/2014/01/09/…
Aside: the code is formatted the way it is because it's the middle of a multi-line raw string literal with embedded leading spaces. Indenting the text changes how it prints.
17

Using match ensures that the expressions $left and $right are each evaluated only once, and that any temporaries created during their evaluation live at least as long as the result bindings left and right.

An expansion which used $left and $right multiple times -- once while performing the comparison, and again when interpolating into an error message -- would behave unexpectedly if either expression had side effects. But why can't the expansion do something like let left = &$left; let right = &$right;?

Consider:

let vals = vec![1, 2, 3, 4].into_iter();
assert_eq!(vals.collect::<Vec<_>>().as_slice(), [1, 2, 3, 4]);

Suppose this expanded to:

let left = &vals.collect::<Vec<_>>().as_slice();
let right = &[1,2,3,4];
if !(*left == *right) {
    panic!("...");
}

In Rust, the lifetime of temporaries produced within a statement is generally limited to the statement itself. Therefore, this expansion is an error:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:5:21
   |
5  |         let left = &vals.collect::<Vec<_>>().as_slice();
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^           - temporary value dropped here while still borrowed
   |                     |
   |                     temporary value does not live long enough

The temporary vals.collect::<Vec<_>>() needs to live at least as long as left, but in fact it is dropped at the end of the let statement.

Contrast this with the expansion

match (&vals.collect::<Vec<_>>().as_slice(), &[1,2,3,4]) {
    (left, right) => {
        if !(*left == *right) {
            panic!("...");
        }
    }
}

This produces the same temporary, but its lifetime extends over the entire match expression -- long enough for us to compare left and right, and interpolate them into the error message if the comparison fails.

In this sense, match is Rust's let ... in construct.

Note that this situation is unchanged with non-lexical lifetimes. Despite its name, NLL does not change the lifetime of any values -- i.e. when they are dropped. It only makes the scopes of borrows more precise. So it does not help us in this situation.

4 Comments

What does this answer add to the existing answer, which already states As you can see, we're calling the function twice.?
IMHO, the other answer is essentially incorrect, or at least leaves out the main reason to use match. Avoiding double evaluation could be done with let left = $left; let right = $right; The essence of the OP's question, I believe, is: why doesn't that work?
I'll second that sentiment. I was left confused about why match was used over let after reading the other answer; this answer does a nice job of walking through the explanation that's alluded to in the comment thread on the other answer. Thank you for writing it up, @John! (The first comment is rather accusatory, so I wanted to leave you some words of encouragement. It's really useful to have both answers!)
Same feeling. The accepted answer only explains why match works, not why it is necessary, as the question asks. This should probably be the accepted answer.

Your Answer

Post as a guest

Required, but never shown

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.