POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook

本記事は、原著者の許諾のもとに翻訳・掲載しております。

  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) + 2 8 + (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 をお読みください。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。