JavaScript的Monad

banq發表於2015-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> {
    // ...
}
<p class="indent">


物件導向語言中如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 class="indent">


有三個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 class="indent">

下面是展示使用這個Identity monad實現計算加法:

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

print(result);
<p class="indent">


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 class="indent">


Nothing 代表一個空值:

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


基本用法也類似identity monad:

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

print(result);
<p class="indent">

與 identity monad主要區別是空值可以自動傳播,當計算任何步驟返回一個Nothing時,所有後續的計算將會忽視,返回Nothing。

下面程式碼中的alert函式就不會被執行,因為前面步驟返回了一個空值。

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

這種方式類似特殊值NaN (not-a-number) ,當計算中間有一個結果是NaN時,NaN值會傳播到整個計算過程:

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


Maybe用於保護因為null空值引起的錯誤,下面案例是返回一個登入使用者的頭像:

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


在一個很長的方法呼叫鏈中不檢查空值的話,如果一個返回結果是null會引起TypeError:

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

改進辦法是使用null檢查,但是這會使得程式碼變得冗長羅嗦,下面程式碼是正確的,但是一行變成了多行程式碼:

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

print(url);
<p class="indent">


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 class="indent">


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 class="indent">


作為陣列和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 class="indent">


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 class="indent">


關於更復雜的Do notation和Chained呼叫(鏈式呼叫)可見原文:

Monads in JavaScript — Curiosity driven

[該貼被banq於2015-07-25 14:58修改過]

相關文章