今天的夜点心关于函数式编程中的 Monad 函子javascript
函数式编程(下面简称 FP),每每被前端们拿来主义地用来解决一些「局部困难」:如使用 rxjs
来处理订阅流;如使用高阶组件来复用逻辑。它在充斥着反作用的应用中默默承担着一个工具的角色,帮上一点小忙,却不太受重视,还时常被曲解。html
「函数式夜点心」系列但愿从动机出发,剥去一些干扰视听的细节和定义,介绍一些 FP 中的概念。但愿籍此能让笔者和你们一块儿对 FP 的优点和困境有更深刻的认识。前端
今天要介绍的概念 Monad (可译做「单子函子」)是为了解决「函数组合」和「异常处理」这两个问题而引入的概念。java
现有以下两个函数 f
和 g
,它们都输入一个数字并输出一个数字。咱们不关心他们的逻辑细节,而仅经过一种简洁的方式来声明他们的输入输出类型: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; // 成功时返回的执行结果
}
复制代码
如今咱们就能够改造 f
和 g
两个函数,让它们返回包含了数字结果的 Maybe
结构。下面声明中的 Maybe Number
表明包装了数字类型的 Maybe
容器:promise
mf::Number -> Maybe Number
mg::Number -> Maybe Number
复制代码
于此同时咱们但愿由他们组合获得的 mh
函数也具备相同的输入输入出类型:安全
mh::Number -> Maybe Number
复制代码
这时原来的组合函数 compose
就不能知足把 mf
和 mg
组合成 mh
的需求了,由于 mf
的返回结果是 Maybe
类型的,不能直接输入给接收数字的 mg
处理。咱们须要一个新的组合函数,姑且把它称做 mcompose
,先不用关心它的实现,只要知道它可以把 mf
和 mg
组合成 mh
就好了:
let mh = mcompose(mg, mf);
复制代码
到这里,对于一些函数式语言而言,其实咱们已经实现了所谓的 Monad:在对上面咱们定义的结构 Maybe
实现了 mcompose
操做以后,Maybe
就成为一个 Monad 了,就是这么简单。
但对于 ES 而言,咱们仍是须要将上述的组合过程改写为链式调用的形式来方便你们理解。把 mf
和 mg
组合成 mh
的逻辑改写成以下的链式结构:
let mh = x => Maybe.of(x).chain(mf).chain(mg)
复制代码
这里的 Maybe
在原先持有 data
和 error
字段的基础上得到了一些额外的方法:
of
方法把输入一个数字,输出包装持有该数字的一个 Maybe
结构chain
方法经过输入的函数(该函数符合 Number -> Maybe Number
的结构),对自身持有的值进行处理,输出一个持有新的结果的 Maybe
实例此外咱们的 Maybe
还须要实现一个 map
方法,来方便咱们将原来输出数字的 f
和 g
转为为输出 Maybe
的 mf
和 mg
:
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)
); // 是否是有点流的感受了
复制代码
在原生的 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 的呢?答案是否认的,根本缘由在于,Promise
的 then
便可以像 map
那样直接处理相似上面 f
这样的函数,又能像 chain
那样处理 mf
那样的函数,它混淆了两个概念,这样的混淆会形成一些本来在其余 Monad 上成立的「重构等式」在 Promise
上不成立,故严格来讲,不能把它算做 Monad (详见 stackoverflow - Why are Promises Monads? 下的第一个回答)。
最后,Monad 是流的雏形。各类流式框架的核心结构都是 Monad ,例如 rx 中的 Observable
,xstream 中的 XStream
,而 most 框架的名字就是由 Monadic Stream 的首字母 mo 和 st 构成的。
为了方便解释,文中简化和减小一些概念,在这里作一下补充:
Maybe
不会像文中那样定义成响应体的结构,而是被分解为两个构造器 Just
和 Nothing
,前者用来包含结果,后者用来表示异常。如在 Haskell 中能够定义为 data Maybe a = Just a | Nothing
。有的语言或框架把这种异常处理的结构命名为 Either
,分为 Right
(正常)和 Left
(异常)两个构造器:data Either a = Right a | Left
。mcompose
方法等同于操做符 >=>
,相似的 Monad 操做符还有 >>=
, >>
,都是与具体的 Monad 分离的方法。这代表咱们并不须要把数据和方法绑定在一块儿才能让 Monad 成立,只是在 ES 等多范式语言中,经过类来实现 Monad 是最天然的方式。chain
的定义是:M a -> (a -> M b) -> b
,便可以将一个包装了 a
类型的结构经过具备 a -> M b
的结构函数,映射获得一个包装了 b
类型的结构。点下方原文连接,能够在 github 中看到对 Maybe
ES 的实现