Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
404 views
in Technique[技术] by (71.8m points)

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

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)
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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

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:


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...