Option.take() usecase?

Why do we need to use option.take() ever? We can always just re-assign moved options. Consider the following example that is able to compile and run even without option.take().

struct Foo<T> {
  bar: Bar<T>,
}

struct Bar<T> {
  t: Option<Box<T>>
}

fn main() {
  let mut foo = Foo{bar:Bar{t:Some(Box::new(3))}};
  let zoo = foo.bar.t;
  foo.bar.t = Some(Box::new(4));

  println!("{} {}", zoo.unwrap(), foo.bar.t.unwrap());
}

You can't if you only have a mutable reference, since it's not possible to move out of a reference.

9 Likes

One common idiom is to use Option::take() to control how/when something is destroyed without requiring unsafe and std::mem::ManuallyDrop.

struct Container {
    field: Option<Target>,
    // ...
}

impl Container {
    fn consume(mut self) {
        let field = self.field.take().unwrap();
        // manually make sure the Target's destructor never gets called.
        std::mem::forget(field);
        // let the other fields be dropped normally when self goes out 
        // of scope
    }
}

struct Target;

impl Drop for Target {
    fn drop(&mut self) {
        println!("Dropped!");
    }
}
1 Like

It might be worth noting that this fails:

   let mut foo = Foo{bar:Bar{t:Some(Box::new(3))}};
   let zoo = foo.bar.t;
-  foo.bar.t = Some(Box::new(4));
+  if true {
+     foo.bar.t = Some(Box::new(4));
+  }

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `foo.bar.t`
  --> src/main.rs:16:35
   |
11 |   let zoo = foo.bar.t;
   |             --------- value moved here
12 |   if true {
13 |     foo.bar.t = Some(Box::new(4));
   |     --------- this reinitialization might get skipped
...
16 |   println!("{} {}", zoo.unwrap(), foo.bar.t.unwrap());
   |                                   ^^^^^^^^^ value used here after move
   |
   = note: move occurs because `foo.bar.t` has type `Option<Box<i32>>`, which does not implement the `Copy` trait

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to previous error

3 Likes

Feel free to add more use cases!

I have a crate that has Option.take() as a core method (not that it could NOT be replaced by something else, but it is still there).

The use case is very simple. Let me give you an example (modified so it is not too specific for my use case) I have this struct:

pub struct NamedValues {
    pub names: Vec<String>,
    pub values: Option<Vec<f64>>,
}

Now, this struct allows me to add values as follows:

impl NamedValues{
   fn push(&mut self, name: String, v: f64){
       self.names.push(name);
       self.values.push(v);
   }
}

So, the two fields are VERY MUCH related to each other and therefore it makes sense to build them together.

Now, the reason this is not a HASH is twofold:

First—and I'm not including this in the example—every time there is a push(), I'll check the data that is being input (e.g., I want it to be lower than the last element being input).

Second, because after building this structure, I want to separate the names from the values. I do this by using take(). In my use case, the values become the state of a simulation and therefore I want them to be easily cloned, copied, transferred, read and written. The names, on the other hand, become useful only for writing the results afterwards (i.e., to keep note of what each value means).

let mut values = named_values.take();
// Write down a CSV with results
println!("{}", &named_values.names);
loop {
   update_values_through_highly_sofisticated_simulation(&mut values);
   println!("{}", values);
}

and the result would be something like

name1, name2, name3
1, 2, 3
4, 5, 6
...

Note that Option::take is arguably just a convenient method-form of the more general std::mem::take, which is yet-again a convenience for calling std::mem::replace(…, Default::default). mem::replace is an immensely useful method; its purpose is, similar to what was already pointed out for Option above, that it works with a mutable reference, yet it gives ownership of the value behind that reference to the caller (who must in turn provide a replacement value).

This method also reveals the harsh truth of what “ownership” really means in Rust; you don’t own the concrete object/thing itself, you’re just entitled to getting back some value of the same type after you “lended” out the original one to some mutable borrow. Thank god Rust is quite strongly typed, otherwise this guarantee would be entirely useless. No, but seriously… ownership isn’t a concept developed for protecting a special “owner” status of a thing, but it’s merely about resource management. The owner of a Foo is entitled to (and obliged to) handle the “disposal” of (exactly) one element of type Foo; for making sure all resources are cleaned up in the end, object identities aren’t really relevant.

Back to the topic at hand… I would also like to provide a fun little application/example of where Option::take can come in handy: You can turn an FnOnce (function that can be called once) into an FnMut that (can be called multiple times but) will panic when called more than one time. FnOnce requires ownership of the function, FnMut is only allowed &mut … access to captured variables; so an Option and .take() are the solution for obtaining ownership through &mut … access; the unwrap() will be causing the panic on the second (and later) call(s):

fn build_fn_mut<Arg, R>(f: impl FnOnce(Arg) -> R) -> impl FnMut(Arg) -> R {
    let mut x = Some(f);
    move |a| x.take().unwrap()(a)
}

// demonstration
fn main() {
    let string = "Hello".to_string();
    let f = |()| {
        println!("dropping string in f");
        drop(string); // can only happen once
    };
    let mut g = build_fn_mut(f);
    g(());
    println!("Expecting panic next…");
    g(());
}

In case you’re wondering why this might ever be useful? Surely, typical APIs would either require only FnOnce in case the aren’t calling the function multiple times, right? Well, one advantage that FnMut has over FnOnce even if you only want to call it once is that you can work with (i.e. you can call) &mut dyn FnMut(…) -> … trait objects, whereas for FnOnce, you’d need a Box, which requires allocation.

7 Likes

Let me try to find an abstract description of that:

It makes me believe that (one of) the abstract use case(es) of Option::take (in combination with Option::unwrap) is to assume ownership at runtime (where the compiler can't determine that we'll only do this "once" (until a new value is provided)).

This applies both to your FnOnce example as well as my if true example (where if true could be a more complex mechanism in practice).

1 Like