Honestly Sam

Nested Options in Rust

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 Options 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. Options 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.

Thoughts? Leave a comment