函数式夜点心:Monad

今天的夜点心关于函数式编程中的 Monad 函子javascript

函数式编程(下面简称 FP),每每被前端们拿来主义地用来解决一些「局部困难」:如使用 rxjs 来处理订阅流;如使用高阶组件来复用逻辑。它在充斥着反作用的应用中默默承担着一个工具的角色,帮上一点小忙,却不太受重视,还时常被曲解。html

「函数式夜点心」系列但愿从动机出发,剥去一些干扰视听的细节和定义,介绍一些 FP 中的概念。但愿籍此能让笔者和你们一块儿对 FP 的优点和困境有更深刻的认识。前端

今天要介绍的概念 Monad (可译做「单子函子」)是为了解决「函数组合」和「异常处理」这两个问题而引入的概念。java

问题描述

现有以下两个函数 fg,它们都输入一个数字并输出一个数字。咱们不关心他们的逻辑细节,而仅经过一种简洁的方式来声明他们的输入输出类型:git

f::Number -> Number

g::Number -> Number
复制代码

如今咱们但愿把他们组合起来,获得一个新的函数 h,让它也成为一个输入数字输出一个数字的函数:github

h::Number -> Number
复制代码

这很简单,构造一个用于组合函数的 compose 工具函数就能够了,好比像下面这样:编程

let compose = (func1, func2) => x => func1(func2(x))

let h = compose(g, f);
复制代码

异常处理的问题

上面的函数 f, g, h 看起来一切正常,可是咱们不能保证输入到它们的值必定合法,有可能输入空值致使报错。FP 认为异常处理不该该打断一段逻辑的执行,因此采用 try-catch 语句来抓错是不可行的。为了作到在处理异常的同时不打断执行,咱们须要经过一种「容器」将函数的结果包装起来,用来标明一个结果是否是存在异常。这里咱们把这种容器命名为 Maybe,由于它具备某种未知性。这个容器能够具备以下相似咱们熟悉的 AJAX 响应数据的结构:数组

interface Maybe<T> { // Maybe 的结构能够像一个 AJAX 请求的响应数据同样
  error?: boolean; // error 代表执行过程是否是有异常
  data?: T; // 成功时返回的执行结果
}
复制代码

如今咱们就能够改造 fg 两个函数,让它们返回包含了数字结果的 Maybe 结构。下面声明中的 Maybe Number 表明包装了数字类型的 Maybe 容器promise

mf::Number -> Maybe Number

mg::Number -> Maybe Number
复制代码

于此同时咱们但愿由他们组合获得的 mh 函数也具备相同的输入输入出类型:安全

mh::Number -> Maybe Number
复制代码

这时原来的组合函数 compose 就不能知足把 mfmg 组合成 mh 的需求了,由于 mf 的返回结果是 Maybe 类型的,不能直接输入给接收数字的 mg 处理。咱们须要一个新的组合函数,姑且把它称做 mcompose,先不用关心它的实现,只要知道它可以把 mfmg 组合成 mh 就好了:

let mh = mcompose(mg, mf);
复制代码

到这里,对于一些函数式语言而言,其实咱们已经实现了所谓的 Monad:在对上面咱们定义的结构 Maybe 实现了 mcompose 操做以后,Maybe 就成为一个 Monad 了,就是这么简单。

但对于 ES 而言,咱们仍是须要将上述的组合过程改写为链式调用的形式来方便你们理解。把 mfmg 组合成 mh 的逻辑改写成以下的链式结构:

let mh = x => Maybe.of(x).chain(mf).chain(mg)
复制代码

这里的 Maybe 在原先持有 dataerror 字段的基础上得到了一些额外的方法:

  • of 方法把输入一个数字,输出包装持有该数字的一个 Maybe 结构
  • chain 方法经过输入的函数(该函数符合 Number -> Maybe Number 的结构),对自身持有的值进行处理,输出一个持有新的结果的 Maybe 实例

此外咱们的 Maybe 还须要实现一个 map 方法,来方便咱们将原来输出数字的 fg 转为为输出 Maybemfmg

let mf = x => Maybe.of(x).map(f)
let mg = x => Maybe.of(x).map(g)
复制代码

好了!像上面这样实现了 of, map, chain 方法且可以持有值的对象,就被称为 Monad。它能帮助咱们解决「函数组合」和「异常处理」的问题,让咱们能够自由安全地组合逻辑,作到函数粒度的逻辑复用:

mh(null) // { error: true };
mh(1) // { data: {正确的返回值} };
mh(1).chain(mh) // 自我组合
mh(1).chain(
  x => Maybe.of(x).map(x => x + 1)
); // 是否是有点流的感受了
复制代码

ES 原生的 Monad

在原生的 ECMAScript 语言中有没有 Monad 呢?咱们熟知的 Array 就是一个,只是它的动机不在「异常处理」,并且它实现的链式方法不叫 chain 而叫 flatMap,下面以 Array 为例替换上文中的 Maybe

let f = x => x + 1
let g = x => x ** 2

let mf = x => Array.of(x).map(f)
let mg = x => Array.of(x).map(g)

let mh = x => Array.of(x).flatMap(mf).flatMap(mg);
复制代码

Array 做为 Monad 为咱们提供了「批量处理数据」和「组合逻辑」的能力。

那另外一个重要的 ES 对象 Promise 是否关于 then 方法成为 Monad 的呢?答案是否认的,根本缘由在于,Promisethen 便可以像 map 那样直接处理相似上面 f 这样的函数,又能像 chain 那样处理 mf 那样的函数,它混淆了两个概念,这样的混淆会形成一些本来在其余 Monad 上成立的「重构等式」在 Promise 上不成立,故严格来讲,不能把它算做 Monad (详见 stackoverflow - Why are Promises Monads? 下的第一个回答)

最后,Monad 是流的雏形。各类流式框架的核心结构都是 Monad ,例如 rx 中的 Observable,xstream 中的 XStream,而 most 框架的名字就是由 Monadic Stream 的首字母 mo 和 st 构成的。

补充

为了方便解释,文中简化和减小一些概念,在这里作一下补充:

  • 文中几处用来描述函数类型的语法是一种叫作 Hindley–Milner 的类型系统
  • 真正的 Maybe 不会像文中那样定义成响应体的结构,而是被分解为两个构造器 JustNothing,前者用来包含结果,后者用来表示异常。如在 Haskell 中能够定义为 data Maybe a = Just a | Nothing。有的语言或框架把这种异常处理的结构命名为 Either,分为 Right (正常)和 Left (异常)两个构造器:data Either a = Right a | Left
  • 在 Haskell 中,上面的 mcompose 方法等同于操做符 >=> ,相似的 Monad 操做符还有 >>=, >>,都是与具体的 Monad 分离的方法。这代表咱们并不须要把数据和方法绑定在一块儿才能让 Monad 成立,只是在 ES 等多范式语言中,经过类来实现 Monad 是最天然的方式。
  • Monad 实际上是以一系列概念做为基础的,这些概念相互继承,每一层会增长一些特性,文中把特性都集中直接到了 Monad 身上: Context(持有数据)=> Pointed Container(持有 of 方法)=> Functor(持有 map 方法)=> Monad(持有 chain 方法)。而对 chain 的定义是:M a -> (a -> M b) -> b,便可以将一个包装了 a 类型的结构经过具备 a -> M b 的结构函数,映射获得一个包装了 b 类型的结构。

点下方原文连接,能够在 github 中看到对 Maybe ES 的实现

github 原文连接

扩展阅读

相关文章
相关标签/搜索