Rust Try 和 Haskell Monad

你说得对,但是单子是——后面忘了。同时,逐步发觉 Rust Try 的真相。

在学 Haskell 前曾经写过一篇关于 Rust Try 特征的文章, 意识到 Rust 的 Try 特征其实描述了 Haskell Monad 类的一个特例。

根据定义,实现一个 Haskell Monad 需要实现两个方法:

return :: a -> m a
bind :: m a -> (a -> m b) -> m b

我们尝试定义一个用于表示计算的泛型类型 m,它接受两个类型参数 b(break) 和 c(continue),其中 c 表示预期结果的类型,b 表示意外退出的类型。据此改写 return 的签名:

m :: * -> * -> *
return :: c -> m b c

这里的 returnc 类型的预期结果转换为 m,因此也可以称作 from_output。对应地,增加一个函数把意外结果转换为 m。我们这里不把它定义为 m b c 上的方法,因为一个意外结果 b 转换为 m 的过程对于计算结果的类型应该可以泛化。

from_residual :: b -> m b t

由于 m 表示的计算可能是预期的(c)或者意外的(b),我们创建这两种类型的一个简易 sum type ControlFlow,并引入一个 m b c 的方法 branch,用于将 m 转换成为这两个类型之一。m 比我们的 ControlFlow 可能要复杂得多。

data ControlFlow b c
    = Break    b
    | Continue c

branch :: m b c -> ControlFlow b c

据此,我们可以导出 bind。这里的 from_residualb 转换成了新的 m b c' 类型。如果 b 有对应的 from_residual 实现,那么我们的 bind 就可以被定义为 m b c 的导出方法。

bind :: m b c -> (c -> m b c') -> m b c'
bind val compute = case branch val of
    Continue output   -> compute       output
    Break    residual -> from_residual residual

以上我们描述的 m b c 恰好对应 Rust 的 Try 特征,returnbranch 刚好就是 Rust Try 的两个方法,而 b 上的 from_residual 刚好就是 FromResidual 特征的方法:

trait Try: FromResidual<Self::B> {
  type C; // Output
  type B; // Residual
  fn from_output(Self::C) -> Self; // return
  fn branch(self) -> ControlFlow<B, C>;
}

trait FromResidual<B> {
  fn from_residual(B) -> Self;
}

同时,return 加上我们导出的 bind 恰好对应 Haskell Monad 的定义。和 Monad 的区别是,我们定义的 Try 的 kind 是 * -> * -> * 而不是更一般的 * -> *,需要显示指定一个类型参数 b

Try 特征对应的 ?try 块也一定程度上对应了 Haskell 为 Monad 设计的 do 表达式:

try {
  let val = computation?;
  let result = do_something_to(val)?;
  result // Ok-wrapping: 自动调用 return / from_output
}
do
  val <- computation
  result <- do_something_to val
  return result