JavaScript的Monad

banq 15-07-25
         

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修改过]

         

3