2015年8月14日
JavaScriptのモナド
(2015-06-07)by Curiosity driven
本記事は、原著者の許諾のもとに翻訳・掲載しております。
モナド とは、一連のステップによって実行する計算を記述する際に使用する、1つのデザインパターンです。 純粋関数型プログラミング言語 では、モナドは 副作用を管理する ために広く利用されていますが、 マルチパラダイム言語では、モナドで複雑性を制御することもできます 。
モナドはデータ型をラップして、空の値を自動的に伝播したり( Maybe モナド)、非同期コードを簡略化したり( 継続 モナド)といった、新たな動作を既存のデータ型に追加します。
一連のコードをモナドと見なすためには、その構造には次に挙げる3つの要素が含まれていなければなりません。
- 型コンストラクタ — 基本的な型に対してモナドの動作を追加した型を作成する機能です。例えば、基本的なデータ型
number
に対して、Maybe<number>
という型を定義します。 unit
関数。これは一定の値をラップして、基本的な型をモナドに変えるものです。.例えば Maybe モナドで、number
型に対して値2
をラップした場合、Maybe<number>
型のMaybe(2)
という値になります。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つの モナド則 には必ず従わなければなりません。
- bind(unit(x), f) ≡ f(x)
- bind(m, unit) ≡ m
- 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ジェネレータの幾つかの異なる適応例について書いています。
- Generating primes in ES6
- Sudoku solver8
- Easy asynchrony with ES6
- Solving riddles with Prolog and ES6 generators
- Pi approximation using Monte Carlo method
継続モナド
継続モナドは非同期のタスクに対して使用されます。幸い、このモナドは Promise オブジェクトとしてES6に実装されているため、実装する必要はありません。
Promise.resolve(value)
は、値をラップしてpromiseオブジェクトを返します (unit
関数)。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の適用例について書いています。
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 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa