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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With