Why does `ref` in a function parameter move out of the ownership of the argument?

So, what cases for which we must introduce the ref in past rust, and in that case, we don't need ref anymore now?

match ergonomics are considered confusing by some (e.g. @H2CO3). If the explicit ref is also confusing, then it looks like we have hit an inherent complexity. In that case, neither option is necessarily better than the other, and there needs to be a different argument if we were to show which one is better.

Currently, yes, the pattern is not a part of the signature. But what I meant is that if the pattern did count as part of the signature, Rust programs would still be locally analyzable. Thus, I think a language change in this direction is not entirely implausible.

2 Likes

That is true :blush: and I would argue that it should not be changed for several reasons.

  1. Backwards compatibility. While probably not directly breaking code it would change APIs for functions that use ref. Although I don't know how often ref in a function parameter is actually used in the wild.
  2. Allowing it just adds a new way to declare taking a reference. Adding a new way of doing the same thing is bad in my opinion if there is no clear indication that the new way is significantly better in some way.
  3. Inconsistency with mut a: String. The mut here does not change the signature nor is there a sensible way to define how it would do that, because 'mut a' in rust is a property of the binding an not of the data. In the same way ref is part of the binding, not of the data.

I think it would be the wrong direction. It would basically do what C++ references do: implicitly take a reference, i.e. without any indication at the call site. This is undesirable because even though it's technically still analyzable by the compiler, it introduces non-locality for the human reader of the code. Now every time I read a function call, I have to go to the function definition or the documentation in order to know whether it consumes or references the argument (and I would have to do this separately for every argument). That makes code a lot harder to understand correctly, and it would lead to a lot of wrong assumptions when reading new code.

9 Likes

There are examples in the Match Ergonomics RFC. In brief, though, in historic Rust, if you wrote:

let s = Some(String::from("ergonomics"));
match &s {
   Some(v) => {}
   _ => {}
}

you would get a compile error, because you cannot move out of an &Option<String>. To write this in a historic Rust compatible form, you had to write:

let s = Some(String::from("ergonomics"));
match &s {
   Some(ref v) => {}
   _ => {}
}

to indicate to Rust that you wanted v to be bound to a reference, not an owning type.

Match ergonomics is a set of rules that allow the compiler to automatically add ref or ref mut to your pattern in circumstances where that's clearly what was intended.

None of that is correct, unfortunately:

You totally can; &T is Copy for all T. The actual compiler error would have been a simple type mismatch between the pattern (which is Option<_>) and the scrutinee value of the match (which is &Option<_>).

No, you could still not match on a reference and get the referent in pre-match-ergonomics Rust. You'd have to drop the reference in the scrutinee and only add the ref inside the pattern.

1 Like

It'd be better to always refer to the official documentaion before giving deductions.

ref binding mode belongs to the identifier pattern. Read the doc if you haven't!

Identifier patterns bind the value they match to a variable. The identifier must be unique within the pattern. The variable will shadow any variables of the same name in scope.

So the last sentence may give the answer:

// case#1: for this
fn show(ref v: String){}
// desugar to
fn show(v: String){
  let ref v = v; // and v is &String by shadowing
}

// case#2: but in your example, there is no shadowing, so you can use s again
let s = "abc".to_string();
let ref v: String = s; // v is &String
// equivalent to case#1
let s = "abc".to_string();
let ref s = s; // and s is &String by shadowing

// equivalent to case#2
fn show(s: String){
    let ref v: String = s; // v is &String, no shadowing and can use s again
}
2 Likes

Mmm, but isn't that equivalent to

fn show(v: String) {
    let v = &v;
}

Having never written a "ref" into any of my Rust code I now wonder if there is any place "ref" is actually required to do something apart from obfuscating function signatures and the like. Perhaps it could have dug me out of some holes if I had been aware of it.

They are the same if you look into the MIR (on the topleft dropdown button). Rust Playground

fn show1(v: String) {
    let v = &v;
}
fn show2(ref v: String) {}
fn show3(v: String) {
    let ref v = v;
}
// MIR
fn show1(_1: String) -> () {
    debug v => _1;                       // in scope 0 at src/main.rs:2:10: 2:11
    let mut _0: ();                      // return place in scope 0 at src/main.rs:2:21: 2:21
    let _2: &std::string::String;        // in scope 0 at src/main.rs:3:9: 3:10
    scope 1 {
        debug v => _2;                   // in scope 1 at src/main.rs:3:9: 3:10
    }

    bb0: {
        _2 = &_1;                        // scope 0 at src/main.rs:3:13: 3:15
        drop(_1) -> bb1;                 // scope 0 at src/main.rs:4:1: 4:2
    }

    bb1: {
        return;                          // scope 0 at src/main.rs:4:2: 4:2
    }
}

Oh good, because rust-analysers in VS Code tells be they are the same as well.

The place where I most often use ref is matching a reference to an enum variant whose fields have a mix of Copy types (e.g. i32) and non-Copy types (e.g. Vec<i32>). This way I can avoid ending up with &i32 (which would result in needing a sprinkling of * dereference operators through the rest of the code) while still referencing the things that can't and shouldn't be moved.

(If the type were a struct then struct.field field access syntax is a possibility, allowing each use site to make its choice of copy or reference, but enums have to be matched, and I often find value in matching structs too to ensure I did not forget to handle one of the fields.)

5 Likes

I appreciate the explanation. But I would love it if you could show a real code example where "ref" is required to do something.

It's never required, because you can always bind the reference and then dereference:

let (x, y) = returns_ref_to_tuple();
let x = *x;
let y = y.clone();

But ref lets you skip the extra steps after the match.

Here's a snippet of actual code from my project all-is-cubes:

let mut value: MinEval = match *self.primitive() {
    ...
    Primitive::Atom(Atom {
        ref attributes,
        color,
        collision,
    }) => MinEval {
        attributes: attributes.clone(),
        voxels: Evoxels::One(Evoxel {
            color,
            selectable: attributes.selectable,
            collision,
        }),
    },
    ...

If I were to use strictly match-ergonomics style then this would have to be:

let mut value: MinEval = match self.primitive() {
    ...
    Primitive::Atom(Atom {
        attributes,
        color,
        collision,
    }) => MinEval {
        attributes: attributes.clone(),
        voxels: Evoxels::One(Evoxel {
            color: *color,
            selectable: attributes.selectable,
            collision: *collision,
        }),
    },
    ...

I find this significantly less clean.

1 Like

Never say never ^^

// try writing this without using `ref`-patterns
// (it's likely only possible if you introduce a case distinction
// over `first_ix.cmp(second_ix)` so that `split_at_mut` can be used)

/// Returns references to `slice[first_ix].0` and `slice[second_ix].1`,
/// but also there’s an `Option` in the way.
fn obscure_function<'s, S, T>(
    slice: &'s mut [Option<(&mut S, &mut T)>],
    first_ix: usize,
    second_ix: usize,
) -> Option<(&'s mut S, &'s mut T)> {
    let Some((&mut ref mut x, _)) = slice[first_ix] else {
        return None;
    };
    let Some((_, &mut ref mut y)) = slice[second_ix] else {
        return None;
    };
    Some((x, y))
}

or maybe

// This one returns an immutable reference for the first,
// and mutable reference for the second part. The same implementation
// does NOT work, if both are supposed to be mutable,
// as after the first reference is created, the discriminant
// can no longer be inspected. (Presumably to ensure soundness
// of the niche optimizations.)
fn super_obscure_function<'s, S, T>(
    slice: &'s mut [Option<(S, T)>],
    first_ix: usize,
    second_ix: usize,
) -> Option<(&'s S, &'s mut T)> {
    let Some((ref x, _)) = slice[first_ix] else {
        return None;
    };
    let Some((_, ref mut y)) = slice[second_ix] else {
        return None;
    };
    Some((x, y))
}

(here's the case that doesn’t work)

4 Likes

Place disjointedness analysis projecting through slice indexing truly is the gift that keeps on giving when it comes to weird code being accepted, isn't it. Also,

That sure is a pattern I never thought would be useful. It's the pattern dual of the expression &mut *x and normally that kind of explicit reborrowing is rarely ever needed for &mut, which automatically reborrows quite eagerly.

Patterns (without changing binding modes from default) are somewhat interesting in that they require Copy and don't replicate the reborrow-on-mention behavior that &mut-valued expressions are usually enhanced with.

2 Likes

I know of no good reason to put a ref pattern in a function parameter pattern.

Just don't do that.

(Indeed, if you're ever puzzled by ref, I find that most of the time the answer is "just don't use ref; you didn't need it".)

2 Likes

By the way, clippy will also recommend against the pattern

warning: `ref` directly on a function argument is ignored. Consider using a reference type instead
 --> src/lib.rs:1:9
  |
1 | fn show(ref v: String){}
  |         ^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#toplevel_ref_arg
  = note: `#[warn(clippy::toplevel_ref_arg)]` on by default
1 Like

How does the pattern ref v:String in the let-binding act? Why it can make the variable be of reference type? Instead, the ref in the pattern will be ignored when the pattern appears in parameter?

It makes the the variable a reference in both cases. It's not ignored. But it doesn't change the function taking the parameter by value.

Notionaly like so.

In both cases the attribute applies to the value expression (right of the assignment, parameter of function) while the pattern determines the variable bindings.

2 Likes

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.