Monad是一种设计模式,使用一系列步骤来描述计算,它们典型地使用在纯函数语言中用于管理副作用,也可使用在多范式语言中用来控制复杂性。
Monad封装类型带来了附加的行为,比如空值(Maybe monad)的自动传播,或简化异步代码(Continuation monad)
为了描述一个Monad结构,需要提供以下三个组件:
1.类型构造器 :一种为基础类型创建monadic类型的特质,比如定义Maybe<number>作为基础类型number的monadic类型。
2.unit函数,这是封装基础类型的值进入一个monad,对于Maybe monad,它封装了number类型的数值2进入Maybe<number>的类型,变成了Maybe(2)。
3.bind函数,能够对monadic值进行链条化操作。
下面TypeScript代码展示了这些普通特性,假设M表示一个Monadic类型。
interface M<T> { } function unit<T>(value: T): M<T> { // ... } function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> { // ... }
|
在面向对象语言中如Javascript中,unit函数能被表示为一个构造器,而bind函数被作为实例方法:
interface MStatic<T> { // constructor that wraps value new(value: T): M<T>; }
interface M<T> { // bind as an instance method bind<U>(transform: (value: T) => M<U>): M<U>; }
|
有三个Monadic法则必须遵循:
1.bind(unit(x), f) ≡ f(x)
2.bind(m, unit) ≡ m
3.bind(bind(m, f), g) ≡ bind(m, x ⇒ bind(f(x), g))
前面两个法则是说, unit是一个中立元素,第三条说,bind必须是可组合的associative , bind绑定顺序并不重要,比如:(8 + 4) + 2 等同于 8 + (4 + 2).
下面案例需要javascript的箭头语法支持,Firefox版本 31以上支持箭头函数,而Chrome版本36并不支持。
Identity monad
这是最简单的monad,它只是封装一个值,Identity 构造器类似unit函数。
function Identity(value) { this.value = value; } Identity.prototype.bind = function(transform) { return transform(this.value); }; Identity.prototype.toString = function() { return 'Identity(' + this.value + ')'; };
|
下面是展示使用这个Identity monad实现计算加法: var result = new Identity(5).bind(value => new Identity(6).bind(value2 => new Identity(value + value2)));
print(result);
|
Maybe Monad
类似identity monad,但是会保存一个代表存在的值在其中。
Just构造器用于封装值。
function Just(value) { this.value = value; }
Just.prototype.bind = function(transform) { return transform(this.value); };
Just.prototype.toString = function() { return 'Just(' + this.value + ')'; };
|
Nothing 代表一个空值:
var Nothing = { bind: function() { return this; }, toString: function() { return 'Nothing'; } };
|
基本用法也类似identity monad:
var result = new Just(5).bind(value => new Just(6).bind(value2 => new Just(value + value2)));
print(result);
|
与 identity monad主要区别是空值可以自动传播,当计算任何步骤返回一个Nothing时,所有后续的计算将会忽视,返回Nothing。下面代码中的alert函数就不会被执行,因为前面步骤返回了一个空值。
var result = new Just(5).bind(value => Nothing.bind(value2 => new Just(value + alert(value2)))); print(result);
|
这种方式类似特殊值NaN (not-a-number) ,当计算中间有一个结果是NaN时,NaN值会传播到整个计算过程: var result = 5 + 6 * NaN; print(result);
|
Maybe用于保护因为null空值引起的错误,下面案例是返回一个登录用户的头像:
function getUser() { return { getAvatar: function() { return null; // no avatar } }; }
|
在一个很长的方法调用链中不检查空值的话,如果一个返回结果是null会引起TypeError:
try { var url = getUser().getAvatar().url; print(url); // this never happens } catch (e) { print('Error: ' + e); }
|
改进办法是使用null检查,但是这会使得代码变得冗长罗嗦,下面代码是正确的,但是一行变成了多行代码: var url; var user = getUser(); if (user !== null) { var avatar = user.getAvatar(); if (avatar !== null) { url = avatar.url; } }
print(url);
|
Maybe 提供了另外一种方式,它可以在遇到空值时停止计算:
function getUser() { return new Just({ getAvatar: function() { return Nothing; // no avatar } }); }
var url = getUser() .bind(user => user.getAvatar()) .bind(avatar => avatar.url);
if (url instanceof Just) { print('URL has value: ' + url.value); } else { print('URL is empty.'); }
|
List monad
List列表monad表示一个懒计算的值的集合列表。
这个monad会unit函数获取一个输入值,返回一个yield在这个值的generator(yield 是用于暂停,然后resume再次开始一个generator 函数 ) ,bind函数会应用transform函数到列表集合中每个元素,然后yield住结果中的所有元素。
function* unit(value) { yield value; } function* bind(list, transform) { for (var item of list) { yield* transform(item); } }
|
作为数组和generator是可被遍历的,bind函数会作用于它们,下面案例是为每个元素前后配对计算其总数的一个懒列表:
var result = bind([0, 1, 2], function (element) { return bind([0, 1, 2], function* (element2) { yield element + element2; }); }); for (var item of result) { print(item); }
|
Continuation monad
Continuation monad是用于实现异步任务,很幸运ES6没有必要这样实现了,因为Promise对象实际就是这种monad的实现.
1.Promise.resolve(value) 封装一个值,返回一个promise (一个 unit 函数).
2.Promise.prototype.then(onFullfill: value => Promise) 获取一个输入参数,一个函数将这个参数值转为不同的promise 然后返回一个promise (也就是bind 函数).
// Promise.resolve(value) will serve as the Unit function // Promise.prototype.then will serve as the Bind function Native promises
var result = Promise.resolve(5).then(function(value) { return Promise.resolve(6).then(function(value2) { return value + value2; }); }); result.then(function(value) { print(value); });
|
关于更复杂的Do notation和Chained调用(链式调用)可见原文:
Monads in JavaScript — Curiosity driven
[该贴被banq于2015-07-25 14:58修改过]