What's the idiomatic way to handle multiple `Option<T>` in Rust?

不想你离开。 提交于 2020-06-27 07:39:09

问题


Since I'm fairly new to Rust, I need guidance on how error handling is done idiomatically. I find the error-handling boilerplate really annoying.

I'm stuck with multiple Option<T>s. It's too verbose to handle each None case manually.

In Haskell, for example, you can chain optional value (Maybe) operations with a variety of operators: fmap, <*>, >>=, etc.:

f x = x * x
g x = x ++ x
main = print $ g <$> show <$> f <$> Just 2

The same looks impossible in Rust. I'm trying to parse a two-character card string into a struct Card:

const FACES: &'static str = "23456789TJQKA";
const SUITS: &'static str = "CDHS";
enum Face { /* ... */ }
enum Suit { C, D, H, S }
struct Card {
    face: Face,
    suit: Suit
}
impl FromStr for Card {
    type Err = ();
    fn from_str(x: &str) -> Result<Self, Self::Err> {
        let mut xs = x.chars();
        let a = chain(xs.next(), |x| FACES.find(x), Face::from_usize);
        let b = chain(xs.next(), |x| SUITS.find(x), Suit::from_usize);
        if let (Some(face), Some(suit)) = (a, b) {
            Ok(Card::new(face, suit))
        } else {
            Err(())
        }
    }
}

This code would look like this in Haskell:

import Data.List (elemIndex)
x = Just 'C'
suits = "CDHS"
data Suit = C | D | H | S deriving Show
fromInt 0 = C
find = flip elemIndex
main = print $ x >>= find suits >>= return . fromInt

Thanks to the chaining via >>= Haskell makes it possible (and easy!) to manipulate the inner value of a monad. In order to achieve something close to that I had to write the chain function, which seems strongly unidiomatic:

fn join<T>(x: Option<Option<T>>) -> Option<T> {
    if let Some(y) = x {
        y
    } else {
        None
    }
}

fn bind<A, B, F>(x: Option<A>, f: F) -> Option<B>
where
    F: FnOnce(A) -> Option<B>,
{
    join(x.map(f))
}

fn chain<A, B, C, F, G>(x: Option<A>, f: F, g: G) -> Option<C>
where
    F: FnOnce(A) -> Option<B>,
    G: FnOnce(B) -> Option<C>,
{
    bind(bind(x, f), g)
}

回答1:


As mentioned, Option and Result have tons of utility methods on them. Additionally, the try operator (?) can also be used for the extremely common case of "return the failure or unwrap the result"

I'd implement FromStr for Face and Suit. Your code would then look like:

impl FromStr for Card {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let face = s[0..1].parse()?;
        let suit = s[1..2].parse()?;

        Ok(Card { face, suit })
    }
}

If you didn't / couldn't, you can use the existing methods on Option. You didn't define Foo::from_usize, so I assume to returns Foo, so it would use map:

fn from_str(s: &str) -> Result<Self, Self::Err> {
    let mut c = s.chars();

    let face = c
        .next()
        .and_then(|c| FACES.find(c))
        .map(Face::from_usize)
        .ok_or(())?;
    let suit = c
        .next()
        .and_then(|c| SUITS.find(c))
        .map(Suit::from_usize)
        .ok_or(())?;

    Ok(Card { face, suit })
}
  • Option::and_then
  • Option::map
  • Option::ok_or

Both of these paths allow you to have useful errors, such as an enum that lets you know if the suit / face was missing / invalid. An error type of () is useless to consumers.

You could also define Suit::from_char and Face::from_char and not leak the implementation of the array out.

Putting it all together:

impl Suit {
    fn from_char(c: char) -> Option<Self> {
        use Suit::*;

        [('c', C), ('d', D), ('h', H), ('s', S)]
            .iter()
            .cloned()
            .find(|&(cc, _)| cc == c)
            .map(|(_, s)| s)
    }
}

enum Error {
    MissingFace,
    MissingSuit,
    InvalidFace,
    InvalidSuit,
}

impl FromStr for Card {
    type Err = Error;

    fn from_str(x: &str) -> Result<Self, Self::Err> {
        use Error::*;

        let mut xs = x.chars();

        let face = xs.next().ok_or(MissingFace)?;
        let face = Face::from_char(face).ok_or(InvalidFace)?;
        let suit = xs.next().ok_or(MissingSuit)?;
        let suit = Suit::from_char(suit).ok_or(InvalidSuit)?;

        Ok(Card { face, suit })
    }
}

fn join<T>(x: Option<Option<T>>) -> Option<T>

This is x.and_then(|y| y)

fn bind<A, B, F>(x: Option<A>, f: F) -> Option<B>
where
    F: FnOnce(A) -> Option<B>,

This is x.and_then(f)

fn chain<A, B, C, F, G>(x: Option<A>, f: F, g: G) -> Option<C>
where
    F: FnOnce(A) -> Option<B>,
    G: FnOnce(B) -> Option<C>,

This is x.and_then(f).and_then(g)

See also:

  • How to implement some convenient methods (e.g., flat_map, flatten) on Option?



回答2:


It seems like you want Option::and_then:

pub fn and_then<U, F>(self, f: F) -> Option<U> 
where
    F: FnOnce(T) -> Option<U>

Examples:

fn sq(x: u32) -> Option<u32> { Some(x * x) }
fn nope(_: u32) -> Option<u32> { None }

assert_eq!(Some(2).and_then(sq).and_then(sq), Some(16));
assert_eq!(Some(2).and_then(sq).and_then(nope), None);
assert_eq!(Some(2).and_then(nope).and_then(sq), None);
assert_eq!(None.and_then(sq).and_then(sq), None);



回答3:


In addition to the other answers, you can also take a look at the monadic expression crates like mdo or map_for. For example with map_for:

fn from_str(x: &str) -> Result<Self, Self::Err> {
    let mut xs = x.chars();
    map_for!{
        ax <- xs.next();
        f  <- FACES.find(ax);
        a  <- Face::from_usize(f);
        bx <- xs.next();
        s  <- SUITS.find(bx);
        b  <- Suit::from_usize (s);
        => Card::new(a, b) }
    .ok_or(Err(()))
}

Full disclosure: I am the author of the map_for crate.




回答4:


Maybe-monadic chaining in Rust's Result is accomplished by the try! macro. Should look something like

fn from_str(x: &str) -> Result<Self, Self::Err> {
    let mut xs = x.chars();
    let a = try!(chain(xs.next(), |x| FACES.find(x), Face::from_usize));
    let b = try!(chain(xs.next(), |x| SUITS.find(x), Suit::from_usize));
    Ok(Card::new(face, suit))
}


来源:https://stackoverflow.com/questions/50731439/whats-the-idiomatic-way-to-handle-multiple-optiont-in-rust

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!