JavaScript的Monad

15-07-25 banq
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> {
    // ...
}
<p>

在面向对象语言中如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>;
}
<p>

有三个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 + ')';
};
<p>

下面是展示使用这个Identity monad实现计算加法:

var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                      new Identity(value + value2)));

print(result);
<p>

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 + ')';
};
<p>

Nothing 代表一个空值:

var Nothing = {
    bind: function() {
        return this;
    },
    toString: function() {
        return 'Nothing';
    }
};
<p>

基本用法也类似identity monad:

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

print(result);
<p>

与 identity monad主要区别是空值可以自动传播,当计算任何步骤返回一个Nothing时,所有后续的计算将会忽视,返回Nothing。

下面代码中的alert函数就不会被执行,因为前面步骤返回了一个空值。

var result = new Just(5).bind(value =>
                 Nothing.bind(value2 =>
                      new Just(value + alert(value2))));
​
print(result);
<p>

这种方式类似特殊值NaN (not-a-number) ,当计算中间有一个结果是NaN时,NaN值会传播到整个计算过程:

var result = 5 + 6 * NaN;
​
print(result);
<p>

Maybe用于保护因为null空值引起的错误,下面案例是返回一个登录用户的头像:

function getUser() {
    return {
        getAvatar: function() {
            return null; // no avatar
        }
    };
}
<p>

在一个很长的方法调用链中不检查空值的话,如果一个返回结果是null会引起TypeError:

try {
    var url = getUser().getAvatar().url;
    print(url); // this never happens
} catch (e) {
    print('Error: ' + e);
}
<p>

改进办法是使用null检查,但是这会使得代码变得冗长罗嗦,下面代码是正确的,但是一行变成了多行代码:

var url;
var user = getUser();
if (user !== null) {
    var avatar = user.getAvatar();
    if (avatar !== null) {
        url = avatar.url;
    }
}

print(url);
<p>

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.');
}
<p>

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);
    }
}
<p>

作为数组和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);
}
<p>

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);
});
<p>

关于更复杂的Do notation和Chained调用(链式调用)可见原文:

Monads in JavaScript — Curiosity driven

[该贴被banq于2015-07-25 14:58修改过]

3
猜你喜欢