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();
}
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(())
}