#monads #macro #do

hado

使用宏实现的 Monadic do 语法

2 个版本

使用旧版 Rust 2015

0.1.1 2018 年 11 月 8 日
0.1.0 2016 年 7 月 16 日

#1076Rust 模式

25 每月下载量
sidekiq-rs 中使用

Apache-2.0

11KB
69

hado

通过 hado! 宏将 Haskell 类似表达式引入 Rust

是什么?

一个小型宏,用于编写 Haskell 类似的 do 表达式,无需太多仪式感

为什么?

Rust 在错误处理方面非常明确(通过 Option 和 Result 类型),但处理所有错误可能会很繁琐,因此此库引入了来自 Haskell 类似语言的组合 monad 模式。

让我们看看一个例子:我们将尝试进行简单的数学运算并组合可能出现的错误。

首先我们定义我们的返回类型,此类型将代表函数的成功或失败。

type MathResult = Option<f64>;
  • 除法:如果除数为 0,则失败
  fn div(x: f64, y: f64) -> MathResult {
      if y == 0.0 {
          None
      } else {
          Some(x / y)
      }
  }
  • 平方根:如果得到负数,则失败
  fn sqrt(x: f64) -> MathResult {
      if x < 0.0 {
          None
      } else {
          Some(x.sqrt())
      }
  }
  • 对数:再次,如果得到负数,则失败
  fn ln(x: f64) -> MathResult {
      if x < 0.0 {
          None
      } else {
          Some(x.ln())
      }
  }

现在我们想获取两个数字,将它们相除,然后获取结果的平方根,最后获取最后一个结果的对数

直接的方式

  fn op(x: f64, y: f64) -> MathResult {
      let ratio = div(x, y);
      if ratio == None {
          return None
      };
      let ln = ln(ratio.unwrap());
      if ln == None {
          return None
      };
      return sqrt(ln.unwrap())
  }

尽管这段代码可以工作,但它很难扩展,并且不是 Rust 的惯用语法,它看起来更像是 Java 中的代码,其中 NoneNULL

更好的方式

  fn op(x: f64, y: f64) -> MathResult {
      match div(x, y) {
          None => None,
          Ok(ratio) => match ln(ratio) {
              None => None,
              Ok(ln) => sqrt(ln),
          },
      }
  }

这个例子更接近 Rust,但它仍然看起来有很多噪音,而且仍然很难扩展

函数式编程的方式

  fn op(x: f64, y: f64) -> MathResult {
      div(x, y).and_then(|ratio|
      ln(ratio).and_then(|ln|
      sqrt(ln)))
  }

这种方式看起来几乎像是我们要做的特殊事情,但那些 and_then 和闭包似乎是不必要的

hado 宏的方式

  fn op(x: f64, y: f64) -> MathResult {
      hado! {
          ratio <- div(x, y);
          ln <- ln(ratio);
          sqrt(ln)
      }
  }

这里我们有一个非常明显的方式来声明我们的意图,没有任何错误处理的迹象,我们只需要添加一个 trait Monad 到 Option(该库中默认已定义)

错误类型无关

现在一些更高级的功能,你可能认为 but what about the try! macro, 它肯定会使事情变得更好, right?,我的回答是 yes,但 try 宏 Result 类型上工作,因此无法更改错误类型(或将其与基于 Option 的函数一起使用)。

但现在我们需要知道导致计算失败的错误是什么。因此,我们将 MathResult 别名更改为 f64 或自定义类型 MathError 的 Result

  #[derive(Debug)]
  pub enum MathError {
      DivisionByZero,
      NegativeLogarithm,
      NegativeSquareRoot,
  }

  type MathResult = Result<f64, MathError>;

因此,现在我们需要更改每个函数,因为现在所有的 NoneSome 构造函数都是类型错误

例如,div 将变为

  fn div(x: f64, y: f64) -> MathResult {
      if y == 0.0 {
          Err(MathError::DivisionByZero)
      } else {
          Ok(x / y)
      }
  }

注意,唯一的变化是

  • Err(MathError::DivisionByZero)
  • 一些 Ok (x / y)

现在我们来检查每个实现的 op 函数

  • 朴素方法:更改所有构造函数、失败检查器,幸运的是 Rust 的类型推断帮助我们避免了更改太多类型声明。
  • 更好的方法:稍微好一些,构造函数相同,但失败检查器被 match 语句替换,但仍然非常冗长。
  • 函数式编程方法:要好得多,我们唯一担心的是新错误类型有某种 and_then,其他所有东西都应该正常工作。
  • hado 方法:与上一个类似,但现在没有需要更改的内容,计算没有对失败框架的关注。

如何使用它?

该宏可以根据在 crate 内部定义的 trait(默认实现为 Option 和 Result)执行一些基本操作

让我们从 Rust 书和 Rust by Example 中取一些示例,并将它们转换为 hado 格式。

Result 类型参考示例

这是原始的基于 try 的错误处理,使用早期返回

  fn write_info(info: &str) -> io::Result<()> {
      let mut file = try!(File::create("my_best_friends.txt"));
      println!("file created");
      try!(file.write_all(format!("rating: {}\n", info.rating).as_bytes()));
      Ok(())
  }

以下是基于 hado 的

  fn hado_write_info(string: &str) -> io::Result<()> {
      hado!{
          mut file <- File::create("my_best_friends.txt");
          println!("file created");
          file.write_all(format!("string: {}\n", string).as_bytes())
      }
  }

注意 ign 关键字是特殊的,它表示丢弃内部值,但在失败的情况下,整个表达式将短路到该错误。由于没有箭头(<-),所以 println 的返回值被完全丢弃,因此可以在此处有任何非失败语句(包括 letuse

多参数构造函数

假设我们有一个 Foo 结构体,它有一个新方法

  fn new(a: i32, b: f64, s: String) -> Foo {
      Foo {
          a: i32,
          b: f64,
          s: String
      }
  }

从普通构造函数创建一个 Option 构造函数,无需太多麻烦

  fn opt_new(a: Option<i32>, b: Option<f64>, s: Option<String>) -> Option<Foo> {
      a <- a;
      b <- b;
      s <- s;
      ret new(a, b, s)
  }

注意,仅通过更改类型签名,就可以将 Option 更改为任何其他单调类型。

您还可以在不进行大量样板操作的情况下进行一些自定义验证

  fn opt_new(a: i32, b: f64, s: String) -> Option<Foo> {
      a <- validate_a(a);
      b <- validate_b(b);
      s <- validate_s(s);
      ret new(a, b, s)
  }

无运行时依赖项