What is the difference between `if let Some(ref x) = y` and `if let Some(x) = y.as_ref()`?

Today I happened to write a code:

let y = Some("".to_string());
let closure = || {
  if let Some(ref x) = y
    && x.is_empty()
  {
    false
  } else {
    true
  }
}

And noticed that the closure is FnOnce, which seems odd since no ownership is taken.

Then I changed the if let line to:

  if let Some(x) = y.as_ref()

The closure now is Fn.

What is the difference here that gives this result?

1 Like

Are you sure it's FnOnce? (Your example doesn't exactly compile so I guess this is what it's supposed to be)

3 Likes

If you hadn’t written the ref pattern, then it would consume y by-value when you match it against a pattern – then written just “Some(x)” – which would mean it can only happen once, hence the closure would be only FnOnce.

So maybe the code you share here is slightly different than the “Some(ref x)” pattern you’re sharing here.

Or maybe you’re getting confused by closure traits… every Fn-closure is in fact also implementing FnOnce, so if you written something like this, you might be drawing wrong conclusions. (Always test the expected compilation errors, too ^^)

A last idea on what could be going on: maybe possibly some IDE functionality may tell you about closure properties / types, and if that’s based on over-simplified heuristics that ignore ref-patterns, it may tell you a difference where actually no relevant difference exists.


Edit: It’s also possible to make an Fn closure look like it’s only FnOnce by passing it through some abstraction. E.g. pass it to a function that expects impl FnOnce(…) -> …, and inside that function you can’t use it like an Fn anymore. Or return it from a function with an opaque impl FnOnce(…) -> … return type, then the caller can only use it as an FnOnce, either. Similarly, if converted to a Box<dyn FnOnce(…) -> …>, of course.

Of course, you’ll simply have to share your full test case for us to really figure out what happened :wink:

2 Likes

Sorry, missed the &&.

The type of the closure was noted by Rust Analyzer. Seems like it cannot correctly identify the closure type?

Thanks. I think it is a Rust Analyzer issue.

1 Like

Inside a function, that's similar to how you can't copy something if you only have a Clone bound, even if what was passed in does actually implement Copy.

But directly passing to a function that expects impl FnOnce(...) actually has further implications than other traits. The same "closure inference override" that we rely on to fix closures that take and return any lifetime inhibits the compiler from attempting to implement FnMut(...) or Fn(...) at all.

Yes for some reason it picks a more restrictive trait than it should.
This PR was the one that added closures inlay hints. There are tests (in this file) that seem to show it should provide the correct closure type.
I couldn't find an issue related to this.

2 Likes

Maybe not "more restrictive". I extended the text area and saw more of the type hint. It says impl FnOnce() -> bool = move(y) for first case, and impl Fn() -> bool = move(&y) for second case. Seems like it "moves" different things.

Submit an issue: RA generates inappropriate type information. · Issue #21398 · rust-lang/rust-analyzer · GitHub

4 Likes