Каков идиоматический способ обработки нескольких `Option‹T›` в Rust?

Поскольку я новичок в Rust, мне нужно руководство о том, как идиоматически выполняется обработка ошибок. Я нахожу шаблон обработки ошибок действительно раздражающим.

Я застрял с несколькими Option<T>s. Это слишком много, чтобы обрабатывать каждый случай None вручную.

В Haskell, например, вы можете связать необязательные операции со значениями (Maybe) с различными операторами: fmap, <*>, >>= и т. д.:

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

То же самое выглядит невозможным в Rust. Я пытаюсь разобрать двухсимвольную строку карты в структуру 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(())
        }
    }
}

Этот код будет выглядеть так в 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

Благодаря цепочке через >>= Haskell позволяет (и легко!) манипулировать внутренним значением монады. Чтобы добиться чего-то близкого к этому, мне пришлось написать функцию chain, которая выглядит строго однотипной:

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

person qwe    schedule 07.06.2018    source источник


Ответы (4)


Как уже упоминалось, Option и Result содержат тонны служебных методов. Кроме того, оператор try (?) также может использоваться для чрезвычайно распространенного случая «вернуть ошибку или развернуть результат».

Я бы реализовал FromStr для Face и Suit. Тогда ваш код будет выглядеть так:

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

Если вы не сделали/не смогли, вы можете использовать существующие методы на Option. Вы не определили Foo::from_usize, поэтому я предполагаю, что он возвращает Foo, поэтому он будет использовать 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 })
}

Оба этих пути допускают наличие полезных ошибок, таких как перечисление, позволяющее узнать, отсутствует ли костюм/лицо/недействительно. Тип ошибки () бесполезен для потребителей.

Вы также можете определить Suit::from_char и Face::from_char и не выдавать реализацию массива.

Собираем все вместе:

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>

Это x.and_then(|y| y)

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

Это 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>,

Это x.and_then(f).and_then(g)

Смотрите также:

person Shepmaster    schedule 07.06.2018

Похоже, вы хотите Option::and_then:

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

Примеры:

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);
person Jorge Israel Peña    schedule 07.06.2018

В дополнение к другим ответам вы также можете взглянуть на контейнеры монадических выражений, такие как mdo или map_for. Например, с 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(()))
}

Полное раскрытие: я являюсь автором ящика map_for.

person Jmb    schedule 07.06.2018

Maybe-монадная цепочка в Result Rust выполняется с помощью макрос try!. Должно выглядеть примерно так

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))
}
person leftaroundabout    schedule 07.06.2018
comment
Теперь, когда ? стабилизировалось, я думаю, что try! уходит. let a = chain(xs.next(), |x| FACES.find(x), Face::from_usize)?; выглядит немного лучше. - person Alec; 07.06.2018
comment
Интересно. Это в основном синтаксический сахар для старого try!? - person leftaroundabout; 07.06.2018