> I prefer keeping functions very "flat"-looking.
I definitely agree. One way I do that is by having an internal function that takes a valid value and a public function that does the validating/error handling.
That doesn't always make sense though. There's a few other idiomatic ways to avoid nesting. Since statements evaluate to values, you can write
let foo = if let Some(foo) = foo {
foo
} else {
// Something that either evaluates to the same type as foo or returns early
}
That's so common there's a special operator for it, ?. It essentially either early returns the sad path or evaluates to the happy path. fn get_foo() -> Option<Foo>;
fn frob() -> Option<Bar> {
let foo = get_foo()?;
let bar = convert_to_bar(foo);
Some(bar)
}
I prefer to use Result to model missing data like cases instead of Option because it composes better. So that might be fn get_foo() -> Option<Foo>;
fn frob() -> Result<Bar, BarNotFound> {
let foo = get_foo().ok_or(BarNotFound)?;
let bar = convert_to_bar(foo);
Some(bar)
}
#[derive(Debug, thiserror::Error)]
#[error("Bar not found")]
struct BarNotFound;
That last bit uses a stdlib macro and a very commonly used external lib macro to save a few lines of repetitive typing.Edit: Also ? doesn't special case Result and Option. You can make your own type conform to the interface (trait) it requires. That would probably be weird though.