【问题标题】:What's the idiomatic way to handle multiple `Option<T>` in Rust?在 Rust 中处理多个 `Option<T>` 的惯用方法是什么?
【发布时间】:2018-06-07 00:34:24
【问题描述】:

由于我对 Rust 还很陌生,因此我需要有关如何以惯用方式完成错误处理的指导。我发现错误处理样板真的很烦人。

我被多个Option&lt;T&gt;s 卡住了。手动处理每个None 案例太冗长了。

例如,在 Haskell 中,您可以将可选值 (Maybe) 操作与各种运算符链接起来:fmap&lt;*&gt;&gt;&gt;= 等:

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

感谢&gt;&gt;= 的链接,Haskell 使得操纵 monad 的内部值成为可能(而且很容易!)。为了达到接近的效果,我不得不编写 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)
}

【问题讨论】:

    标签: rust monads option maybe


    【解决方案1】:

    如前所述,OptionResult 实用方法。此外,try 运算符(?)也可以用于“返回失败或解包结果”这种极其常见的情况

    我会为FaceSuit 实现FromStr。您的代码将如下所示:

    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_charFace::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)

    另见:

    【讨论】:

      【解决方案2】:

      好像你想要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);
      

      【讨论】:

        【解决方案3】:

        除了其他答案之外,您还可以查看 mdomap_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 crate 的作者。

        【讨论】:

          【解决方案4】:

          Maybe-Rust 的Result 中的一元链是由the try! macro 完成的。应该看起来像

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

          【讨论】:

          • 现在? 已经稳定下来,我认为try! 即将退出。 let a = chain(xs.next(), |x| FACES.find(x), Face::from_usize)?; 看起来好多了。
          • 有趣。这基本上是旧 try! 的语法糖吗?
          • @leftaroundabout 实际上更好,因为它由Try trait 提供支持,因此更加灵活。例如,它适用于Option,当它稳定后,它将可用于用户类型。
          猜你喜欢
          • 2021-05-05
          • 2018-12-22
          • 2023-02-23
          • 2013-06-15
          • 2014-01-28
          • 2022-08-18
          • 2019-02-26
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多