Take() vs as_ref().take()

fn main() {
    let x = Some(2);
    let y = x.as_ref().take();
}

take() works on mutable reference.
Here I'm expecting an error like "cannot borrow as mutable". Why is that I don't see this error?

But the following gives the expected error which can be fixed with mut x

fn main() {
    let x = Some(2);
    let y = x.take();
}

I presume as_ref() to be an immutable borrow. Looks like my understanding is incorrent.
Can someone please explain x.as_ref().take() contrasting with x.take().

I think you might be confusing this with the AsRef trait. But look at the docs for Option: https://doc.rust-lang.org/std/option/enum.Option.html#method.as_ref

pub const fn as_ref(&self) -> Option<&T>;

So you're takeing only temporary Option returned from the as_ref.

(Which is not actually doing anything useful, since you already have ownership of the temporary option, and thus don't need to take it.)

As a side point, please never use AsRef::as_ref outside of generic code. It's not there to be used as a replacement for &, and will just make your code more confusing and more likely to break in future (if more AsRef implementations are added).

EDIT: Filed a clippy lint suggestion, Add a lint for `Option::take` on a temporary · Issue #8618 · rust-lang/rust-clippy · GitHub

10 Likes

Let me further illustrate the interaction that “[…] you're takeing only temporary Option returned from the as_ref.”, as @scottmcm was explaining above:

The code

let y = x.as_ref().take();

desugars to

let y = Option::take(&mut Option::as_ref(&x));

where the types involved are as follows

                                  x  :       Option<i32>
                                 &x  :      &Option<i32>
                  Option::as_ref(&x) :      Option<&i32>
             &mut Option::as_ref(&x) : &mut Option<&i32>
Option::take(&mut Option::as_ref(&x)):      Option<&i32>
                                    y:      Option<&i32>

Execution does:

  • create an immutable reference &x that points to the variable x

  • calling Option::as_ref looks at the value behind that reference, sees that it’s Some(2), and produces a reference to the contained value 2. This reference is then wrapped into a new Some(…) producing a temporary value of type Option<&i32>. (The temporary is an owned Option, but the value it contains is a reference. So the outer enum part can be mutated, but the value 2 behind the reference will still be immutable.)

  • &mut … now produces a mutable reference to that temporary value. As explained above &mut Option<&i32> allows mutating the option (e.g. make it point so some other i32, or turn it into None), but not the target of the contained reference; and also recall that the Option is a newly created Option, not the same one as x

  • Option::take turns the temporary value that used to contain Some(/* reference to the `2` contained in `x` */) into None as a side-effect, while returning this Some(/* reference to the `2` contained in `x` */) value.
    before:


    after:

  • (after assigning the result to y, the temporary variable is dropped)


As explained above, the take operation is “not actually doing anything useful”. More precisely, the combination Option::take(&mut /* some owned temporary value */) doesn’t do anything useful; Option::take is useful to obtain ownership over something you only have mutable access to (by “taking” it away); but for an owned value as the Option<&i32> returned from Option::as_ref, you already have ownership (ownership of the Option enum, not ownership of the i32 that its Some-variant’s field might be pointing to)

20 Likes

Thanks @scottmcm, it's making sense now. But I still have other half of doubt lingering in my mind.
So this temporary Option from as_ref() would be immutable. If immutable, then takeing should give "cannot borrow as mutable" error, since take expects &mut.

pub fn take(&mut self) -> Option<T>

Temporary variables, i.e. “temporaries”, the places that contain intermediate results while evaluating a statement, are always mutable.

4 Likes

But why do I see "cannot borrow x as mutable" error when x is a owned value.

let x = Some(2);
let _ = x.take();

Just saw your post Take() vs as_ref().take() - #5 by steffahn. That makes sense. Thanks.

1 Like

Well, x is not a temporary variable, but an ordinary, explicitly declared, variable. Those are immutable by default and would need to be declared mutable. OTOH,

let _ = Some(2).take();

works, because then the Some(2) is stored in an [anonymous] temporary variable, and those are always mutable (there would’t be any syntactical way to declare their mutability otherwise, anyways.)

The way I see it is that "temporaries are always a thing in motion. Who knows what might happen to them downstream. Hence temporaries being mutable makes sense. I wish these things are documented somewhere.

If I up-level a bit, you can think of the whole "mutable NAND shared" behaviour of Rust as a codification of the general idea that "if nobody can tell that you modified something, it doesn't cause problems if you do".

For example, this fails to compile:

    let x = Some(3);
    x.take(); // ERROR
    println!("{:?}", x);

Since x was declared non-mut, and thus that println "expects" it to not have changed.

But if we were to "give away" our ownership by calling something that moves,

    let x = Some("hello".to_string());
    drop(x);
    println!("{:?}", x); // ERROR

Then we're not allowed to look at x any more, and thus we can't tell whether it gets modified.

So if we instead call something that gives us something back,

    let x = Some("hello".to_string());
    std::convert::identity(x).take();

Then x has still been moved and we're not allowed to care what happened to it. And similarly, the function can't tell whether we're modifying what came back, so we're allow to modify it -- until we put it into something, like a non-mut let, that makes things start to care again. (Although this example again hits the "take specifically isn't useful here", as steffahn elaborated on above.)

And while that's obviously not a technically-perfect explanation of what the rules are precisely, hopefully it'll give you a bit of intuition on why the rules are the way they are.

3 Likes

They are.

4 Likes

for posterity, the same documentation on doc.rust-lang.org: Expressions - The Rust Reference

1 Like

Whoops, wrong link, sorry. I didn't even notice it didn't point to the official reference (which it seems to be a copy of).

1 Like