Matthias Endler from corrode.dev wrote an article about his best advice for the circumstance when you want to use the question mark operator on an Option
but the function returns a Result
. Consider this an extension to that article, so I recommend reading that first.
For single cases of an Option
, I agree with the conclusion to use let else
as your first go-to, but it can get unwieldy for more complex data structures. Here I present some tricks to handle nested options, and try to answer some additional questions:
- What is the best way to access that (maybe) value all the way down the structure?
- How can you go on to extract a second?
- How can you do either of these things without handling every
None
value separately?
First Tries
Rust v1.80.1
Here is the dummy data structure. How can I read from field1
and field2
?
#[derive(Default, Debug)] pub struct Data { pub outer: Option<Outer>, } #[derive(Default, Debug)] pub struct Outer { pub inner: Option<Inner>, pub field3: Option<bool>, } #[derive(Default, Debug)] pub struct Inner { pub part: Option<Part>, } #[derive(Default, Debug)] pub struct Part { pub field1: Option<String>, pub field2: Option<u8>, }
To begin, I pretend that let else
and methods on Option
do not exist.
fn a() -> Result<String, &'static str> { let data = Data::default(); let e = "got a None"; let result = match data.outer { None => Err(e), Some(outer) => match outer.inner { None => Err(e), Some(inner) => match inner.part { None => Err(e), Some(part) => match part.field1 { None => Err(e), Some(field) => Ok(field), }, }, }, }; result }
It works, but it is so long! What if we don't worry about the Result
until the end, and just use a cheeky .ok_or()
?
fn b() -> Result<String, &'static str> { let data = Data::default(); let result = match data.outer { None => None, Some(outer) => match outer.inner { None => None, Some(inner) => match inner.part { None => None, Some(part) => part.field1, }, }, }; result.ok_or("got a None") }
One fewer let
statement, one fewer indent. Is this really an improvement? How about our new friend, let else
?
fn c() -> Result<String, &'static str> { let data = Data::default(); let e = "got a None"; let Some(outer) = data.outer else { return Err(e); }; let Some(inner) = outer.inner else { return Err(e); }; let Some(part) = inner.part else { return Err(e); }; let Some(field) = part.field1 else { return Err(e); }; println!("{:#?}", field); Ok(field) }
Yay, no more nesting! But it is still pretty long, and maybe I don't need to have all these intermediate variables, if all I want is field
. Perhaps Option
has some methods that will help. There is a whole box of methods on Option
, and consequently many different ways to do the same thing. Here are two.
fn d1() -> Result<String, &'static str> { let data = Data::default(); data.outer .map_or(None, |o| o.inner) .map_or(None, |o| o.part) .map_or(None, |o| o.field1) .ok_or("got a None") }
Now that's not bad! We have eradicated the nesting, we have cut the line count again, and removed the unnecessary variable creations!
fn d2() -> Result<String, &'static str> { let data = Data::default(); data.outer.unwrap_or_default() .inner.unwrap_or_default() .part.unwrap_or_default() .field1.ok_or("got a None") }
Here we go!! unwrap
and friends are something familiar, so we are just chaining function calls which do things that mostly make sense to a rookie. However, what we are all here for is the question mark. How can we get ?
to work on Option
s when the function returns a Result
? Voilà:
Best Attempt
fn f() -> Result<String, &'static str> { let data = Data::default(); (|| data.outer?.inner?.part?.field1)().ok_or("got a None") }
You need a function to return an Option
in order for ?
to work, so just make one! Here, I have a closure which implicitly returns an Option
, so the ?
works inside the closure declaration. This is my prettiest and most favourite example. Unfortunately, I still cannot call ?
on the final option, and must still choose to handle it in the ways Mr. Endler describes.
Extracting the Second
In practice, you are likely to require more than one field from data
, but as soon as you try this with the solution from f()
, you get an error, because we have already moved data
into the first closure.
fn g() -> Result<String, &'static str> { let data = Data::default(); let the_first = (|| data.outer?.inner?.part?.field1)().ok_or("no field1")?; let the_second = (|| data.outer?.inner?.part?.field2)().ok_or("no field2")?; Ok(format!("{the_first:?} {the_second:?}")) } // this errors on the second closure, because data was moved into the first.
So then what to do? Pass the structure by reference to the closure, instead of by value. Option
s already have AsRef
implemented, but your top level struct won't, so I add it here.
// This makes the final solution pretty, but isn't strictly needed. impl AsRef<Data> for Data { fn as_ref(&self) -> &Data { &self } }
Leading us to a compiling version:
fn h() -> Result<String, &'static str> { let data = Data::default(); let the_first = (|| data.as_ref().outer.as_ref()?.inner.as_ref()?.part.as_ref()?.field1.as_ref())().ok_or("no field1")?; let the_second = (|| data.as_ref().outer.as_ref()?.inner.as_ref()?.part.as_ref()?.field2)().ok_or("no field2")?; Ok(format!("{the_first:?} {the_second:?}")) }
It's not so pretty anymore, but it works. I wonder if the language team would consider some sugar that allowed for an &
in post-fix as a short-hand for .as_ref()
, where if impl AsRef<YourStruct> for YourStruct
holds, you can do this instead:
let the_first = (|| data&.outer&?.inner&?.part&?.field1&)().ok_or("no field1")?;
Conclusion
I have one more example to flesh out all the possibilities we have:
fn i() -> Result<String, &'static str> { let data = Data::default(); let maybe_the_first = (|| data.as_ref().outer.as_ref()?.inner.as_ref()?.part.as_ref()?.field1.as_ref())(); let the_second = (|| data.as_ref().outer.as_ref()?.inner.as_ref()?.part.as_ref()?.field2)().ok_or("field2 is empty")?; let Some(the_third) = (|| data.outer?.field3)() else { panic!("This is an abomonation!") } if the_third { match maybe_the_first { Some(the_first) => Ok(format!("{the_first:?}")), None => Ok(format!("Never mind")) } } else { Ok(format!("{the_second:?}")) } }
Here, we want the system to panic if the_third
is None
. We also leave the_first
as an Option
, because both Some
and None
lead to Ok
results. Also, I am not calling .as_ref()
on the final field2
and field3
values because they are Copy
. Finally, since data
is not used after the variable declarations, I can consume it in the closure for the_third
.
Notice that we can still use the let else
pattern originally recommended! Generally, let else
is more flexible than .ok_or()?
because it could be that your function does not return a Result
, or that a None
requires you to panic.