JavaScriptのモナド

  1. 恒等モナド
  2. Maybeモナド
  3. リストモナド
  4. 継続モナド
  5. Do記法
  6. 連鎖呼び出し

モナドとは、一連のステップによって実行する計算を記述する際に使用する、1つのデザインパターンです。純粋関数型プログラミング言語 では、モナドは副作用を管理するために広く利用されていますが、マルチパラダイム言語では、モナドで複雑性を制御することもできます

モナドはデータ型をラップして、空の値を自動的に伝播したり(Maybeモナド)、非同期コードを簡略化したり(継続モナド)といった、新たな動作を既存のデータ型に追加します。

一連のコードをモナドと見なすためには、その構造には次に挙げる3つの要素が含まれていなければなりません。

  1. 型コンストラクタ — 基本的な型に対してモナドの動作を追加した型を作成する機能です。例えば、基本的なデータ型numberに対して、Maybe<number>という型を定義します。
  2. unit関数。これは一定の値をラップして、基本的な型をモナドに変えるものです。.例えばMaybeモナドで、number型に対して値2をラップした場合、Maybe<number>型のMaybe(2)という値になります。
  3. bind関数。これは、モナド値の操作を連鎖させます。

次に示すTypeScript上のコードは、一般的な関数のシグネチャを表しています。ここでMは、モナド型を表しています。

interface M<T> {
​
}
​
function unit<T>(value: T): M<T> {
    // ...
}
​
function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...

注: このbind関数は、Function.prototype.bind関数と同じものではありません。後者はES5ネイティブの関数です。部分的に適用する関数や、this値に束縛する関数を作成する場合に使います。

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

次に示す3つのモナド則には必ず従わなければなりません。 

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))

上の2つの規則は、unitがニュートラルな要素であることを示しています。3番目の規則は、bindは結合型であることを表しています。結合の順序は問いません。これは、次に示す2つの加算の結果が同じになることと共通しています。(8 + 4) + 28 + (4 + 2)では、どちらも結果は同じです。

以下で示すコーディング例は、アロー関数の構文をサポートしていることを前提としています。Firefox(バージョン31)はアロー関数をネイティブでサポートしていますが、Google Chrome(バージョン36)はこの関数をサポートしていません

恒等モナド

恒等モナドは、最も単純なモナドです。値を単にラップするだけです。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 + ')';

恒等モナドを使った加算処理の例を次に示します。

  • アロー関数 ((a => 42)() === 42)
var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                      new Identity(value + value2)));
​
print(result);

Maybeモナド

Maybeモナドは恒等モナドと似ていますが、値を格納するだけでなく、値が存在しない状態を表すこともできます。

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';
    }

基本的な使い方は、恒等モナドと同じです。

  • アロー関数 ((a => 42)() === 42)
var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));
​
print(result);

恒等モナドとの主な違いは、Maybeモナドは空の値を伝播することです。あるステップでNothingを返すと、モナドはそれ以降の演算をスキップして、Nothingを返します。

次に示すコーディング例では、alert関数は実行されません。その前のステップで空の値が返されるからです。

  • アロー関数 ((a => 42)() === 42)
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値のチェックを組み込めばいいのですが、そうするとコードが冗長になりがちです。コードは正しくても、1行追加するとそれを何度も実行することになるからです。

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

Maybeモナドはこんな場面の新たな解決策となります。空の値を検出すると、演算処理を中止します。

  • アロー関数 ((a => 42)() === 42)
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.');
}

リストモナド

リストモナドは、非決定計算の値のリストを表します。

このモナドのunit関数は値を1つ取り、その値を生成するジェネレータを返します。bind関数は全ての要素に対してtransform関数を適用し、その結果からすべての要素を生成します

function* unit(value) {
    yield value;
}
​
function* bind(list, transform) {
    for (var item of list) {
        yield* transform(item);
    }

配列やジェネレータは、反復可能なオブジェクトのため、bind関数を適用できます。下記の例は、ペアになっている全ての要素の和のLazyListを作成します。

  • ES6ジェネレータ((function*() { yield 42; }))
  • for-ofループ(for(var i of (function*() { yield 42; })() ) { }; true) _
  • 反復可能オブジェクト配列(for(var i of [42]) { }; true)_
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);
}

下記の記事は、JavaScriptジェネレータの幾つかの異なる適応例について書いています。

  1. Generating primes in ES6
  2. Sudoku solver8
  3. Easy asynchrony with ES6
  4. Solving riddles with Prolog and ES6 generators
  5. Pi approximation using Monte Carlo method

継続モナド

継続モナドは非同期のタスクに対して使用されます。幸い、このモナドはPromiseオブジェクトとしてES6に実装されているため、実装する必要はありません。

  1. Promise.resolve(value)は、値をラップしてpromiseオブジェクトを返します (unit関数)。
  2. Promise.prototype.then(onFullfill: value => Promise) は引数として関数を受け取ります。その関数は、ある値を別のpromiseオブジェクトに変換し、返します(bind関数)。
// Promise.resolve(value) will serve as the Unit function

// Promise.prototype.then will serve as the Bind function

  • ネイティブpromise (‘Promise’ in window)
var result = Promise.resolve(5).then(function(value) {
    return Promise.resolve(6).then(function(value2) {
        return value + value2;
    });
});

result.then(function(value) {
    print(value);
});

Promiseは基本的な継続モナドのいくつかの機能を拡張します。もし、thenが(promiseオブジェクトではない)単なる値を返した場合、モナド内で自動的に値をラップして、与えられた値で成功となる promiseオブジェクトを返します

さらに、promiseではエラー伝播も異なります。継続モナドは、計算処理中は1つの引数しか渡せません。しかし、promiseの場合、処理が成功した時に渡す値とエラーが発生した時に渡す値の2つの値を同時に保持しています。この点はEitherモナドと似ています。エラーは、thenメソッドへの2番目のコールバックを使用するか、特別な.catchメソッドを使用してキャプチャします。

下記の記事は、promiseの適用例について書いています。

  1. Easy asynchrony with ES6
  2. Simple AMD loader in 30 lines of code

Do記法

Haskellには、モナドコードを書く時に利用できる do記法という特別なシンタックスシュガーがあります。doをキーワードに始まるコードブロックをbind関数によって呼び出し可能なオブジェクトに変換します

JavaScriptでdo記法を模倣したのが、ES6ジェネレータです。すっきりとした、同期的に見えるコードができます。

下記のように、Maybeモナドで使用したコード例では、bind関数を直接呼び出しています。

  • アロー関数 ((a => 42)() === 42)
  • ES6ジェネレータ ((function*() { yield 42; }))
var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

print(result);

上記と同じコードをジェネレータで表すと下記になります。yieldをそれぞれ呼び出すところで、モナドのラップ処理を取り消して、元の値を取得しています。

var result = doM(function*() {
    var value = yield new Just(5);
    var value2 = yield new Just(6);
    return new Just(value + value2);
}());

print(result);

この小さなルーチンは、ジェネレータをラップした後、yieldに渡される値でbindを呼び出します。

function doM(gen) {
    function step(value) {
        var result = gen.next(value);
        if (result.done) {
            return result.value;
        }
        return result.value.bind(step);
    }
    return step();
}

このルーチンは、他のモナド、例えば継続モナドと同時に使用することができます。

Promise.prototype.bind = Promise.prototype.then;

var result = doM(function*() {
    var value = yield Promise.resolve(5);
    var value2 = yield Promise.resolve(11);
    return value + value2;
}());

result.then(print);

他のモナドとの整合性を保つため、then関数はbindというエイリアスとして記述される場合があります。

ジェネレータとpromiseを同時に使用する方法についてさらに詳しく知りたい方は、Easy asynchrony with ES6(ES6で簡単に非同期処理)をお読みください。

連鎖呼び出し

モナドコードをさらに単純化するもう1つの方法は、プロキシの使用です。

下記の関数は、モナドのインスタンスをラップして、不明なプロパティのアクセスや関数の起動をモナド内の値へ自動的に転送するプロキシオブジェクトを返します。

  • アロー関数 ((a => 42)() === 42)
function wrap(target, unit) {
    target = unit(target);
    function fix(object, property) {
        var value = object[property];
        if (typeof value === 'function') {
            return value.bind(object);
        }
        return value;
    }
    function continueWith(transform) {
        return wrap(target.bind(transform), unit);
    }
    return new Proxy(function() {}, {
        get: function(_, property) {
            if (property in target) {
                return fix(target, property);
            }
            return continueWith(value => fix(value, property));
        },
        apply: function(_, thisArg, args) {
            return continueWith(value => value.apply(thisArg, args));
        }
    });
}

このラッパーは、将来的に空になる可能性のあるオブジェクトの参照に安全にアクセスするために使用することができます。これは、CoffeeScriptの存在演算子 (?.)と同じような動作をします。

function getUser() {
    return new Just({
        getAvatar: function() {
            return Nothing; // no avatar
        }
    });
}

var unit = value => {
    // if value is a Maybe monad return it
    if (value === Nothing || value instanceof Just) {
        return value;
    }
    // otherwise wrap it in Just
    return new Just(value);
}

var user = wrap(getUser(), unit);

print(user.getAvatar().url);

アバターは存在しませんが、urlの呼び出しは成功し、空の値を作成します。

このラッパーは、通常の関数呼び出しを継続モナドに引き上げる際にも使用できます。下記のコードはある特定のアバターを使用している友達の人数を返します。この例はメモリ内のデータを操作しているように見えますが、実際には非同期データを操作しています。

Promise.prototype.bind = Promise.prototype.then;

function User(avatarUrl) {
    this.avatarUrl = avatarUrl;
    this.getFriends = function() {
        return Promise.resolve([
            new User('url1'),
            new User('url2'),
            new User('url11'),
        ]);
    };
}

var user = wrap(new User('url'), Promise.resolve);

var avatarUrls = user.getFriends().map(u => u.avatarUrl);

var length = avatarUrls.filter(url => url.contains('1')).length;

length.then(print);

プロパティのアクセスや関数の呼び出しは全てモナドに引き上げられたため、このラッパーは常にpromiseを作成し、もはや単純な値は作成しないことに注意してください。

ES6プロキシについてさらに詳しく知りたい方はArray slicesをお読みください。