Search code examples
rustoption-typelazy-initialization

How can I lazy initialize/fill an `Option` with a fallible initializer?


Suppose I have a struct containing an Option<Resource>, where Resource is some type I need to work with that needs to allocate external resources (in the actual case, GPU memory) and so might fail. From within a method, I want to attempt the allocation if it hasn't been done already, but propagate or handle the error if it does fail.

If it weren't for the failure case, Option::get_or_insert_with would be perfect. As it is, the tidiest solution I have thought of involves an unwrap(), which is inelegant since it looks like a potential panic:

struct Container {
    resource: Option<Resource>,
    ...
}

impl Container {
    ...
    fn activate(&mut self) -> Result<(), Error> {
        if self.resource.is_none() {
            self.resource = Some(Resource::new()?);
        }
        let resource: &mut Resource = self.resource.as_mut().unwrap();
        // ... now do things with `resource` ...
        Ok(())
    }
    ...
}

Is there a way to initialize the Option with less fuss than this? To be clear, I'm not solely looking for avoiding unwrap(), but also overall readability. If an alternative is much more complex and indirect, I'd rather stick with this.


Complete example code (on Rust Playground):

#[derive(Debug)]
struct Resource {}
#[derive(Debug)]
struct Error;

impl Resource {
    fn new() -> Result<Self, Error> {
        Ok(Resource {})
    }

    fn write(&mut self) {}
}

#[derive(Debug)]
struct Container {
    resource: Option<Resource>,
}

impl Container {
    fn new() -> Self {
        Self { resource: None }
    }

    fn activate(&mut self) -> Result<(), Error> {
        if self.resource.is_none() {
            self.resource = Some(Resource::new()?);
        }
        self.resource.as_mut().unwrap().write();
        Ok(())
    }
}

fn main() {
    Container::new().activate();
}

Solution

  • Indeed, get_or_insert_with() but returning Result<&mut T, E> is what you could use to simplify your code. However, as you already discovered Option doesn't have a e.g. try_get_or_insert_with() method.

    Other workarounds would be similarly verbose. However, you could use a match and get_or_insert(), to avoid the unwrap() like this:

    fn activate(&mut self) -> Result<(), Error> {
        let res = match &mut self.resource {
            Some(res) => res,
            None => self.resource.get_or_insert(Resource::new()?),
        };
        res.write();
    
        Ok(())
    }
    

    If this is used frequently, then you could also define your own try_get_or_insert_with() trait method, and implement it for Option<T>.

    trait TryGetOrInsert<T> {
        fn try_get_or_insert_with<E, F>(&mut self, f: F) -> Result<&mut T, E>
        where
            F: FnOnce() -> Result<T, E>;
    }
    
    impl<T> TryGetOrInsert<T> for Option<T> {
        fn try_get_or_insert_with<E, F>(&mut self, f: F) -> Result<&mut T, E>
        where
            F: FnOnce() -> Result<T, E>,
        {
            match self {
                Some(value) => Ok(value),
                None => Ok(self.get_or_insert(f()?)),
            }
        }
    }
    

    Then now you can simplify your activate() method, to the following:

    fn activate(&mut self) -> Result<(), Error> {
        let res = self.resource.try_get_or_insert_with(|| Resource::new())?;
        res.write();
    
        Ok(())
    }