Skip to main content

Combinators in Rust

·495 words·3 mins
Table of Contents

Rust 中的 OptionResult 类型和 Haskell 中的 MaybeEither 十分相似,并且借鉴了许多函数式编程理念

写在前面的扯淡 #

Rust 是一门没有异常处理的语言,对比同是没有异常处理的 Go。在 Go 中是通过多值返回的形式,返回异常信息

inputFile, inputError := os.Open("input.dat")
if inputError != nil {
    fmt.Printf("An error occurred on opening the inputfile\n")
    return // exit the function on error
}
defer inputFile.Close()

因为 Go 有 Null(nil) 值,所以它可以这么任性。而 Rust 中则是通过 Result(Option) 来做的,它相当于一个箱子,里面包裹了正常的返回值或者异常。我们可以通过一些 combinator function 来对此类型进行进一步的处理,最后将其拆开得到结果值或者异常。代码写起来就像是一个 pipeline

本文以 Result 为例(其定义如下)介绍这些 combinator function

enum Result<T, E> {
   Ok(T),
   Err(E),
}

and_then #

and_then combinator 当且仅当枚举类型 ResultOk(T) 时才会调用作为参数的 closure。

let res: Result<u8, &'static str> = Ok(5);
let value = res.and_then(|n: u8| Ok(n * 2));
assert_eq!(Ok(10), value);

// 与下面的写法等价

let value = match res {
    Ok(n) => Ok(n * 2),
    Err(e) => Err(e),
};
assert_eq!(Ok(10), value);

// 如果为 Err 则不会调用
let res: Result<u8, &'static str> = Err("error");
let value = res.and_then(|n: u8| Ok(n * 2));
assert_eq!(value, res);

我们可以将多个 and_then 进行串联

let res: Result<u8, &'static str> = Ok(0);
let value = res.and_then(|n: u8| {
    if n == 0 {
        Err("cannot divide by zero")
    } else {
        Ok(n)
    }
}).and_then(|n: u8| Ok(2 / n));
assert_eq!(Err("cannot divide by zero"), value);

在 F# 中这种编程范式被称为 Railway Oriented Programming,其使用的是 bind 函数来完成

let bind f opt =  
  match opt with
  | Some v -> f v
  | None -> None

在 Haskell 中可以认为 Result 实现了 Monad 的 typeclass

Prelude> let divby x = if x == 0 then Nothing else Just(2 / x)
Prelude> Just(0) >>= divby
Nothing
Prelude> Just(2) >>= divby
Just 1.0

and_then 的源码如下

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn and_then<U, F: FnOnce(T) -> Result<U, E>>(self, op: F) -> Result<U, E> {
    match self {
        Ok(t) => op(t),
        Err(e) => Err(e),
    }
}

map #

map combinator 可以用于转换,注意这个和 Iteratormap 不同。Result 类型的 map 函数当且仅当枚举类型 ResultOk(T) 时才会调用作为参数的 closure。好吧,这玩意听起来貌似和 and_then 一样

// use `and_then`
let res: Result<u8, &'static str> = Ok(5);
let value = res.map(|n: u8| n * 2);
assert_eq!(Ok(10), value);

// use `map`
let res: Result<u8, &'static str> = Ok(5);
let value = res.and_then(|n: u8| Ok(n * 2));
assert_eq!(Ok(10), value);

看起来貌似也和 and_then 的示例差不多。不过,请仔细看 closure 的返回值。map 总是会将返回值包装成 OK,所以这里我们直接返回 n * 2,而在 and_then 中我们需要返回 Ok(n * 2)。说白了就是 Haskell 中的 Functor

Prelude> fmap (*2) (Just 5)
Just 10
Prelude> fmap (*2) (Nothing)
Nothing

Rust 中 map 的源码如下

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Result<U,E> {
    match self {
        Ok(t) => Ok(op(t)),
        Err(e) => Err(e)
    }
}

map_err #

map_err 是一个相对于 map 的函数,它只在 Result 实际为 Err(E) 的时候才会调用 closure

use std::io::{Error, ErrorKind};

fn main() {
    let res: Result<u8, Error> = Err(Error::new(ErrorKind::Other, "oh no!"));

    let value = res.map(|n: u8| n * 2)
        .map_err(|_e: Error| "find a mistake");

    assert_eq!(Err("find a mistake"), value);
}

or_else #

既然有相对于 mapmap_err,那么也有相对于 and_thenor_else

let res: Result<u8, &'static str> = Err("oh no!");

let value = res.or_else(|_s: &str| Ok(2))
    .or_else(|_s: &str| Err("oh no!!"));

assert_eq!(Ok(2), value);

Conclusion #

简单来说就是 map 可以将 OK(T) 转换成 OK(U)and_then 可以将 OK(T) 变成 OK(U) 或者 Err(F)

推荐阅读 Functors, Applicatives, And Monads In Pictures
扩展阅读 Option Monads in Rust 注意此文章中的 Rust 代码版本比较低