翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。通过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,但愿能够帮助你们在学习函数式编程的道路上走的更顺畅。比心。javascript

译者团队(排名不分前后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyao前端

JavaScript 轻量级函数式编程

附录 B: 谦虚的 Monad

首先,我坦白:在开始写如下内容以前我并不太了解 Monad 是什么。我为了确认一些事情而犯了不少错误。若是你不相信我,去看看 这本书 Git 仓库 中关于本章的提交历史吧!java

我在本书中囊括了全部涉及 Monad 的话题。就像我写书的过程同样,每一个开发者在学习函数式编程的旅程中都会经历这个部分。git

尽管其余函数式编程的著做差很少都把 Monad 做为开始,而咱们却只对它作了简要说明,并基本以此结束本书。在轻量级函数式编程中我确实没有遇到太多须要仔细考虑 Monad 的问题,这就是本文更有价值的缘由。可是并非说 Monad 是没用的或者是不广泛的 —— 偏偏相反,它颇有用,也很流行。github

函数式编程界有一个小笑话,几乎每一个人都不得不在他们的文章或者博客里写 Monad 是什么,把它拎出来写就像是一个仪式。在过去的几年里,人们把 Monad 描述为卷饼、洋葱和各类各样古怪的抽象概念。我确定不会重蹈覆辙!编程

一个 Monad 仅仅是自函子 (endofunctor) 范畴中的一个 monoid小程序

咱们引用这句话来开场,因此把话题转到这个引言上面彷佛是很合适的。但是才不会这样,咱们不会讨论 Monad 、endofunctor 或者范畴论。这句引言不只故弄玄虚并且华而不实。微信小程序

我只但愿经过咱们的讨论,你再也不惧怕 Monad 这个术语或者这个概念了 —— 我曾经怕了很长一段时间 —— 并在看到该术语时知道它是什么。你可能,也只是可能,会正确地使用到它们。安全

类型

在函数式编程中有一个巨大的兴趣领域:类型论,本书基本上彻底远离了该领域。我不会深刻到类型论,坦白的说,我没有深刻的能力,即便干了也吃力不讨好。微信

可是我要说,Monad 基本上是一个值类型。

数字 42 有一个值类型(number),它带有咱们依赖的特征和功能。字符串 "42" 可能看起来很像,可是在编程里它有不一样的用途。

在面向对象编程中,当你有一组数据(甚至是一个单独的离散值),而且想要给它绑上一些行为,那么你将建立一个对象或者类来表示 "type"。接着实例就成了该类型的一员。这种作法一般被称为 “数据结构”。

我将会很是宽泛的使用数据结构这个概念,并且我判定,当咱们在编程中为一个特定的值定义一组行为以及约束条件,而且将这些特征与值一块儿绑定在一个单一抽象概念上时,咱们可能会以为颇有用。这样,当咱们在编程中使用一个或多个这种值的时候,它们的行为会天然的出现,而且会使它们更方便的工做。方便的是,对你的代码的读者来讲,是更有描述性和声明性的。

Monad 是一种数据结构。是一种类型。它是一组使处理某个值变得可预测的特定行为。

回顾第 8 章,咱们谈到了函子(functor):包括一个值和一个用来对构成函子的数据执行操做的类 map 实用函数。Monad 是一个包含一些额外行为的函子(functor)。

松散接口

实际上,Monad 并非单一的数据类型,它更像是相关联的数据类型集合。它是一种根据不一样值的须要而用不一样方式实现的接口。每种实现都是一种不一样类型的 Monad。

例如,你可能阅读 "Identity Monad"、"IO Monad"、"Maybe Monad"、"Either Monad" 或其余形形色色的字眼。他们中的每个都有基本的 Monad 行为定义,可是它根据每一个不一样类型的 Monad 用例来继承或者重写交互行为。

但是它不只仅是一个接口,由于它不仅是使对象成为 Monad 的某些 API 方法的实现。对这些方法的交互的保障是必须的,是 monadic 的。这些众所周知的常量对于使用 Monad 提升可读性是相当重要的;另外,它是一个特殊的数据结构,读者必须所有阅读才能明白。

事实上,这些 Monad 方法的名字和真实接口受权的方式甚至没有一个统一的标准;Monad 更像是一个松散接口。有些人称这些方法为 bind(..),有些称它为 chain(..),还有些称它为 flatMap(..),等等。

因此,Monad 是一个对象数据结构,而且有充足的方法(几乎任何名称或排序),至少知足了 Monad 定义的主要行为需求。每一种 Monad 都基于最少数量的方法来进行不一样的扩展。可是,由于它们在行为上都有重叠,因此一块儿使用两种不一样的 Monad 仍然是直截了当和可控的。

从某种意义上说,Monad 更像是接口。

Maybe

在函数式编程中,像 Maybe 这样涵盖 Monad 是很广泛的。事实上,Maybe Monad 是另外两个更简单的 Monad 的搭配:Just 和 Nothing。

既然 Monad 是一个类型,你可能认为咱们应该定义 Maybe 做为一个要被实例化的类。这虽然是一种有效的方法,可是它引入了 this 绑定的问题,因此在这里我不想讨论;相反,我打算使用一个简单的函数和对象的实现方式。

如下是 Maybe 的最简单的实现:

var Maybe = { Just, Nothing, of/* 又称:unit,pure */: Just };

function Just(val) {
    return { map, chain, ap, inspect };

    // *********************

    function map(fn) { return Just( fn( val ) ); }
    // 又称:bind, flatMap
    function chain(fn) { return fn( val ); }
    function ap(anotherMonad) { return anotherMonad.map( val ); }

    function inspect() {
        return `Just(${ val })`;
    }
}

function Nothing() {
    return { map: Nothing, chain: Nothing, ap: Nothing, inspect };

    // *********************

    function inspect() {
        return "Nothing";
    }
}

注意: inspect(..) 方法只用于咱们的示例中。从 Monad 的角度来讲,它并无任何意义。

若是如今大部分都没有意义的话,不要担忧。咱们将会更专一的说明咱们能够用它作什么,而不是过多的深刻 Monad 背后的设计细节和理论。

全部的 Monad 同样,任何含有 Just(..)Nothing() 的 Monad 实例都有 map(..)chain(..)(也叫 bind(..) 或者 flatMap(..))和 ap(..) 方法。这些方法及其行为的目的在于提供多个 Monad 实例一块儿工做的标准化方法。你将会注意到,不管 Just(..) 实例拿到的是怎样的一个 val 值, Just(..) 实例都不会去改变它。全部的方法都会建立一个新的 Monad 实例而不是改变它。

Maybe 是这两个 Monad 的结合。若是一个值是非空的,它是 Just(..) 的实例;若是该值是空的,它则是 Nothing() 的实例。注意,这里由你的代码来决定 "空" 的意思,咱们不作强制限制。下一节会详细介绍这一点。

可是 Monad 的价值在于不论咱们有 Just(..) 实例仍是 Nothing() 实例,咱们使用的方法都是同样的。Nothing() 实例对全部的方法都有空操做定义。因此若是 Monad 实例出如今 Monad 操做中,它就会对 Monad 操做起短路(short-circuiting)做用。

Maybe 这个抽象概念的做用是隐式地封装了操做和无操做的二元性。

不同凡响的 Maybe

JavaScript Maybe Monad 的许多实现都包含 nullundefined 的检查(一般在 map(..)中),若是是空的话,就跳过该 Monad 的特性行为。事实上,Maybe 被声称是有价值的,由于它自动地封装了空值检查得以在某种程度上短路了它的特性行为。

这是 Maybe 的典型说明:

// 代替不稳定的 `console.log( someObj.something.else.entirely )`:

Maybe.of( someObj )
.map( prop( "something" ) )
.map( prop( "else" ) )
.map( prop( "entirely" ) )
.map( console.log );

换句话说,若是咱们在链式操做中的任何一环获得一个 null 或者 undefined 值,Maybe 会智能的切换到空操做模式 —— 它如今是一个 Nothing() Monad 实例! —— 把剩余的链式操做都中止掉。若是一些属性丢失或者是空的话,嵌套的属性访问能安全的抛出 JS 异常。这是很是酷的并且很实用。

可是,咱们这样实现的 Maybe 不是一个纯 Monad。

Monad 的核心思想是,它必须对全部的值都是有效的,不能对值作任何检查 —— 甚至是空值检查。因此为了方便,这些其余的实现都是走的捷径。这是可有可无的。可是当学习一些东西的时候,你应该先学习它的最纯粹的形式,而后再学习更复杂的规则。

我早期提供的 Maybe Monad 的实现不一样于其余的 Maybe,就是它没有空置检查。另外,咱们将 Maybe 做为 Just(..)Nothing() 的非严格意义上的结合。

等一下,若是咱们没有自动短路,那 Maybe 是怎么起做用的呢?!?这彷佛就是它的所有意义。

不要担忧,咱们能够从外部提供简单的空值检查,Maybe Monad 其余的短路行为也仍是能够很好的工做的。你能够在以前作一些 someObj.something.else.entirely 属性嵌套,可是咱们能够作的更 “正确”:

function isEmpty(val) {
    return val === null || val === undefined;
}

var safeProp = curry( function safeProp(prop,obj){
    if (isEmpty( obj[prop] )) return Maybe.Nothing();
    return Maybe.of( obj[prop] );
} );

Maybe.of( someObj )
.chain( safeProp( "something" ) )
.chain( safeProp( "else" ) )
.chain( safeProp( "entirely" ) )
.map( console.log );

咱们设计了一个用于空值检查的 safeProp(..) 函数,并选择了 Nothing() Monad 实例。或者把值包装在 Just(..) 实例中(经过 Maybe.of(..))。而后咱们用 chain(..) 替代 map(..),它知道如何 “展开” safeProp(..) 返回的 Monad。

当遇到空值的时候,咱们获得了一连串相同的短路。只是咱们把这个逻辑从 Maybe 中排除了。

无论返回哪一种类型的 Monad,咱们的 map(..)chain(..) 方法都有不变且可预测的反馈,这就是 Monad,尤为是 Maybe Monad 的好处。这难道不酷吗?

Humble

如今咱们对 Maybe 和它的做用有了更多的了解,我将会在它上面加一些小的改动 —— 我将经过设计 Maybe + Humble Monad 来添加一些转折而且加一些诙谐的元素。从技术上来讲,Humble(..) 并非一个 Monad,而是一个产生 Maybe Monad 实例的工厂函数。

Humble 是一个使用 Maybe 来跟踪 egoLevel 数字状态的数据结构包装器。具体来讲,Humble(..) 只有在他们自身的水平值足够低(少于 42)到被认为是 Humble 的时候才会执行生成的 Monad 实例;不然,它就是一个 Nothing() 空操做。这听起来真的和 Maybe 很像!

这是一个 Maybe + Humble Monad 工厂函数:

function Humble(egoLevel) {
    // 接收任何大于等于 42 的数字
    return !(Number( egoLevel ) >= 42) ?
        Maybe.of( egoLevel ) :
        Maybe.Nothing();
}

你可能会注意到,这个工厂函数有点像 safeProp(..),由于,它使用一个条件来决定是选择 Maybe 的 Just(..) 仍是 Nothing()

让咱们来看一个基础用法的例子:

var bob = Humble( 45 );
var alice = Humble( 39 );

bob.inspect();                          // Nothing
alice.inspect();                        // Just(39)

若是 Alice 赢得了一个大奖,如今是否是在为本身感到自豪呢?

function winAward(ego) {
    return Humble( ego + 3 );
}

alice = alice.chain( winAward );
alice.inspect();                        // Nothing

Humble( 39 + 3 ) 建立了一个 chain(..) 返回的 Nothing() Monad 实例,因此如今 Alice 再也不有 Humble 的资格了。

如今,咱们来用一些 Monad :

var bob = Humble( 41 );
var alice = Humble( 39 );

var teamMembers = curry( function teamMembers(ego1,ego2){
    console.log( `Our humble team's egos: ${ego1} ${ego2}` );
} );

bob.map( teamMembers ).ap( alice );
// Humble 队列:41 39

因为 teamMembers(..) 是柯里化的,bob.map(..) 的调用传入了 bob 自身的级别(41),而且建立了一个被其他的方法包装的 Monad 实例。在 这个 Monad 中调用的 ap(alice) 调用了 alice.map(..),而且传递给来自 Monad 的函数。这样作的效果是,Monad 的值已经提供给了 teamMembers(..) 函数,而且把显示的结果给打印了出来。

然而,若是一个 Monad 或者两个 Monad 其实是 Nothing() 实例(由于它们自己的水平值过高了):

var frank = Humble( 45 );

bob.map( teamMembers ).ap( frank );

frank.map( teamMembers ).ap( bob );

teamMembers(..) 永远不会被调用(也没有信息被打印出来),由于,frank 是一个 Nothing() 实例。这就是 Maybe monad 的做用,咱们的 Humble(..) 工厂函数容许咱们根据自身的水平来选择。赞!

Humility

再来一个例子来讲明 Maybe + Humble 数据结构的行为:

function introduction() {
    console.log( "I'm just a learner like you! :)" );
}

var egoChange = curry( function egoChange(amount,concept,egoLevel) {
    console.log( `${amount > 0 ? "Learned" : "Shared"} ${concept}.` );
    return Humble( egoLevel + amount );
} );

var learn = egoChange( 3 );

var learner = Humble( 35 );

learner
.chain( learn( "closures" ) )
.chain( learn( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( learn( "map/reduce" ) )
.map( introduction );
// 学习闭包
// 学习反作用
// 歇息递归

不幸的是,学习过程看起来已经缩短了。我发现学习一大堆东西而不和别人分享,会使自我太膨胀,这对你的技术是不利的。

让咱们尝试一个更好的方法:

var share = egoChange( -2 );

learner
.chain( learn( "closures" ) )
.chain( share( "closures" ) )
.chain( learn( "side effects" ) )
.chain( share( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( share( "recursion" ) )
.chain( learn( "map/reduce" ) )
.chain( share( "map/reduce" ) )
.map( introduction );
// 学习闭包
// 分享闭包
// 学习反作用
// 分享反作用
// 学习递归
// 分享递归
// 学习 map/reduce
// 分享 map/reduce
// 我只是一个像你同样的学习者 :)

在学习中分享。是学习更多而且可以学的更好的最佳方法。

总结

说了这么多,那什么是 Monad ?

Monad 是一个值类型,一个接口,一个有封装行为的对象数据结构。

可是这些定义中没有一个是有用的。这里尝试作一个更好的解释:Monad 是一个用更具备声明式的方式围绕一个值来组织行为的方法。

和这本书中的其余部分同样,在有用的地方使用 Monad,不要由于每一个人都在函数式编程中讨论他们而使用他们。Monad 不是万金油,但它确实提供了一些有用的实用函数。

【上一章】翻译连载 | 附录 A:Transducing(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

iKcamp官网:https://www.ikcamp.com
访问官网更快阅读所有免费分享课程:
《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》
《iKcamp出品|基于Koa2搭建Node.js实战项目教程》
包含:文章、视频、源代码