Understandable usage of clone()

Hi,
I wonder there is other ways to do more effectively in this exapmle code:

pub fn do_something(&mut self, op: Op) -> Result<(), MyError> {
    match op.clone() {
        Op::Rename(op) => {
       // many lines...
        }
        Op::Put(op) => {
       // many lines...
        }
        Op::Delete(op) => {
       // many lines...
        }
    }

    // Since the pattern match may early-return with `?`, this function should be called here.
    create_log(&op);
    Ok(())
}

In this case, I need clone() in the first line of the method for create_log(&op), which comes after the long pattern match, but this is at least not easy to understand, and may not be efficient.
How can I avoid this kind of code? Thanks in advance.

Depending on the types involved and what all the // many lines in the match do, you might be able to use

pub fn do_something(&mut self, op: Op) -> Result<(), MyError> {
    match &op {
        Op::Rename(op) => {
       // many lines...
        }
        Op::Put(op) => {
       // many lines...
        }
        Op::Delete(op) => {
       // many lines...
        }
    }

    // Since the pattern match may early-return with `?`, this function should be called here.
    create_log(&op);
    Ok(())
}
5 Likes

you could match against &op then use &Op:: in each match expression. Clearly you aren't consuming (with into) inside the matches otherwise you would Need the clone.

Thank you! After some refactoring, it works with match &op and create_log(&op).

Looks like I didn't understand the relationship between pattern match and consumption of its value. Basically, for example, can I think that just pattern matching for the Option value does not consume its inner value?

1 Like

This behavior is known as match ergonomics. Suppose you have an Option<String>, and you match it against the pattern Some(s). If you use match option, s will be a String; if you use match &mut option, s will be a &mut String; and if you use match &option, s will be a &String. Effectively, the kind of reference you use in the match is transferred to the inner bindings. In this case, using match &op causes the patterns Op::Rename(op), Op::Put(op), etc., to extract the inner op by reference.

1 Like

So, how the inner value is extracted corresponds to how I make the pattern match, right? That makes sense, and matches to my intuition during coding. Thank you!

1 Like

The non-confusing ways to express the same pattern match are:

match &op {
    &Op::Rename(ref op) => {
        ...
    }
}

and

match op {
    Op::Rename(ref op) => {
        ...
    }
}

In both cases, the pattern has the exact same type as the value, i.e. if you are matching on a reference, the pattern is an explicit reference (the first case), and if you are matching against a value, then the pattern is a value too (the second snippet).

In both cases, the inner ref annotations tell the compiler not to move the matched inner value into that particular binding, so if you use it on all bindings, it will always take a reference and you can still use your op after the match.

Pattern matching "ergonomics" has enough subtle problems that I regard it as a misfeature, and usually recommend turning it off completely. This can be done via Clippy, by adding the #![deny(clippy::pattern_type_mismatch)] directive to your main file (lib.rs or main.rs etc).

2 Likes

If you match on a reference, its like matching on the value it points at, except that the value is not consumed and all of the fields become references.

I don't consider it a misfeature. Sure, you have to learn what matching on a reference does, but once you know what it means, I don't think its confusing and I find it a pretty convenient way to use match.

6 Likes

I find match ergonomic easier to understand than ref patterns. Mostly because no new syntax needs to be learned, and the backwards nature of patterns might be confusing to beginners.

Especially if you “read” &x as something like “ref. (to) x” already, then learning a new pattern syntax that literally spells ref x but is considered to have the dual meaning is inherently confusing.

Also, matching against all the other patterns "takes something away" from the value, like a layer of struct or enum, or an indirection, whereas the ref pattern adds a layer of indirection back.

Finally, the use of ref in match means that the value being matched on isn't consumed, even though you write it as-if it's passed by value. Understanding this properly very often requires a better understanding of match as a place expression context, which is also nontrivial for beginners IMO; whilst other kinds of match operations very commonly make sense even if you were to believe that the value you match on is unconditionally moved into the match. (Especially if the structs or enums you work with implement Copy whenever possible and if you don't use _ too much in patterns.)

3 Likes

Some examples may clarify


#[test]
fn examples() {
    let y = (String::from("string"), 5u8);

    if let (ref y_str, ref y_u8) = &y { // borrows
        println!("{} {}", y_str, y_u8);
    }
    println!("y still valid {:?}", y);

    if let (y_str, y_u8) = y { // consumes
        println!("{} {}", y_str, y_u8);
    }
    // println!("y not valid {:?}", y); // fails to compile

    let y = (String::from("string"), 5u8);

    match &y { // borrows
        (ref y_str, ref y_u8) => { println!("{} {}", y_str, y_u8)}
        _ => {}
    }
    println!("y still valid {:?}", y);
    match y { // consumes
        (y_str, y_u8) => { println!("{} {}", y_str, y_u8)}
        _ => {}
    }
    // println!("y not valid {:?}", y); // fails to compile

    let oy = Option::Some((String::from("string"), 5u8));

    if let Some((ref y_str, ref y_u8)) = &oy { // borrows
        println!("{} {}", y_str, y_u8);
    }
    println!("y still valid {:?}", oy);
    match &oy { // borrows
        &Some((ref y_str, ref y_u8)) => { println!("{} {}", y_str, y_u8)}
        _ => {}
    }
    println!("y still valid {:?}", oy);

    if let Some((y_str, y_u8)) = oy { // consumes
        println!("{} {}", y_str, y_u8);
    }
    // println!("y not valid {:?}", oy); // fails to compile

    let oy = Option::Some((String::from("string"), 5u8));
    match oy { // consumes
        Some((y_str, y_u8)) => { println!("{} {}", y_str, y_u8)}
        _ => {}
    }
    // println!("y not valid {:?}", oy); // fails to compile
}

1 Like

Great examples! This is very helpful, thank you so much.

Until you reach the weird places, and then people start wondering about the quirks they see. But this is shared to all syntactic sugars that "cheat".

1 Like

At this point it sounds like do_something could take an &Op?

1 Like

But the problem is: they'll need to learn the "backwardness" of patterns anyway. And match ergonomics tries to hide the undeniable, causing confusion. Using the same syntax for different (opposite) purposes isn't great, either. It leads to inconsistency: it sometimes can be used, at other times it can't, and it isn't intuitive by any measure what that depends on.

It doesn't particularly suggest "by-value", no. By this reasoning, method calls are also confusing because &self and &mut selfmethods can be called on a value of type Self without explicitly taking the reference. Yet this is something that people are OK with, so it's not really fair to use it as an argument in favor of match ergonomics.

I don't really buy this. For one, infallible patterns on product types don't "take away" anything (eg. matching on a tuple and binding its fields is exactly the same as accessing them with numbered field syntax). Second, one can even think of ref as taking away the need to consume by-value. Finally, why isn't the same behavior a problem in the case of dereferencing a reference/pointer explicitly? Prefix & adds a layer of reference, prefix * takes it away. Why is that OK in prefix operators but not
in patterns? It seems to me like you are cherry-picking reasons to support match ergonomics, but these arguments aren't coherent with the rest of the language as it currently stands.

3 Likes

Yeah, you're right. Thank you!

I think (at least this is my own personal feeling) that people are okay with methods because they're immediate: when I look at vec.push(), for example, I see it as (&mut vec).push(). The compiler also sees them this way (autoref takes place before we call things places, no pun intended). Yes, technically every variable referencing/pointer dereferencing produces a place, but it is much easier to think of them as just doing an "operation" (intuitively, I think of &v as one "unit", even though I know it's technically incorrect). But seeing a match that has its address taken, I see a variable that is referenced but nothing happens. This is... weird. And takes time to get used to.

1 Like

"Your milage may vary" always applies when talking about intuitiveness. The example linked if "surprising behavior" is exactly what I would have expected, now and when first learning about pattern matching. I'm not saying that means it is definitely intuitive, but that the only objective measure would be some sort of scientific trial.

So long as any surprising behavior is a compiler error, it's not really any big deal.

What I will complain about is the endless confusion between using &x, &*x, x.as_ref(), x.as_deref() and various nestings for different types of x. It would be nice if something always worked! (But I get why it's a problem)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.