Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2010-03-06

JSDeferredで考えるモナド

16:52

という素敵な記事を見て、おもしろかったので JavaScript Monad とかでググってみたら、↓の記事を発見した。

僕は Haskell をかじった程度には知っていた (しかしモナドは説明できるかと言われれば自信が無い) ので、概念としては理解できたもりなんだけど、いかんせん英語が難解なので訳すのは諦めて、自分なりの解釈を書いてみることにする。(間違ってたらやさしく指摘してくれるとありがたいです)

元記事は dojo.Deferred での説明だけど、ここでは JSDeferred で考える。単純に僕が dojo.Deferred を使ったことがないから。


Deferredモナド

Monad - Enjoy Programming を参考にした。

まず、JavaScript変数を Deferred 型 (モナド) とそれ以外という2つの型に分ける。

モナドの定義として、return と bind という2つの関数がある。


return はモナドでない値を引数に取って、モナドでラップして返す関数。

JSDeferred で書いてみるならこんな感じ。(return については元記事の dojo.Deferred の書き方のほうが直感的)

Deferred.monadReturn = function(val) {
  return Deferred.next(function() { return val }); // Deferred 型のインスタンスを返す
}

bind は2つの引数を取る。第一引数はモナド型の値。第二引数は、モナドでない値を取ってモナド型を返す関数。返り値はモナド型。

JSDeferred にあてはめるなら、

Deferred.monadBind = function(deferred, func) {
  return deferred.next(func); // func には deferred でラップされた普通の値が渡される
}

となる。つまり単なる Deferred.prototype.next メソッドのこと。ただし Deferred.prototype.next に渡す関数は「Deferred でない型を取って Deferred 型を返す」こともできるし、「Deferred でない型を取って Deferred でない型を返す」こともできる。これは Deferred.prototype.next の内部でまず func を実行し、その結果が Deferred 型ならそれを返し、Deferred 型でなければ上の monadReturn 関数相当のもので結果を包む、という操作を行っているため。(JavaScript が Haskell 違って型がないからこういうことができる。とりあえず Deferre.prototype.next の中で使われているぶんにはどっちでも良いと自分は解釈した)


モナド則

次にモナド則というものを考える。return と bind はモナド則を満たすように定義されていなければならない。

モナド則の一つめは、Deferred.monadBind(Deferred.monadReturn(val), func)func(val) と同じであるということ (ただし func は Deferred 型を返すとする)。上の定義に従って書いてみる。

Deferred.monadBind(Deferred.monadReturn(val), func) 

== Deferred.next(function(){ return val }).next(func) // func には val が渡される

== func(val)

同じであることが確認できた。(今 JSDeferred のソースを見たら、Deferred.call(func,val) というのを使えば func が Deferred 型を返しても普通の値を返してもうまくいくっぽい)


モナド則の二つめは、Deferred.monadBind(deferred, Deferred.monadReturn)deferred と等価であるということ。確認してみる。

Deferred.monadBind(deferred, Deferred.monadReturn)

== deferred.next(function(val){ return Deferred.next(function(val) {return val}) });

== deferred.next(function(val){ return val }); // 厳密に同じではないけど、val をラップした Deferred 型という点で同じ

== deferred

ちょっとチートな感じがするけど、全体として Deferred 型で、ラップされた値は deferred がラップした値と同じだと考えるといい。


モナド則の3つめは、Deferred.monadBind( Deferred.monadBind(deferred, func1), func2)Deferred.monadBind( deferred, function(x){ return Deferred.monadBind(func1(x), func2) }) と同じであるということ。これは複雑なので2つに分けて考える。

Deferred.monadBind( Deferred.monadBind(deferred, func1), func2)

== Deferred.next(function(){ return Deferred.monadBind(deferred, func1) }).next(func2)

== Deferred.monadBind(deferred, func1).next(func2)

== deerred.next(func1).next(func2)
Deferred.monadBind( deferred, function(x){ return Deferred.monadBind(func1(x), func2) })

== deferred.next(function(x){ return Deferred.monadBind(func1(x), func2) })

== deferred.next(function(x){ return func1(x).next(func2) })

== deferred.next(func1).next(func2)

というわけで、3つのモナド則が満たされた。


モナドから出ることはできない

一旦普通の値を Deferred 型でラップしてしまうと、通常のコンテキストにその値を戻すことはできない。ラップされた値を操作するのは、Deferred.prototype.next を通してしかできない。これは Haskell のモナドを使ってるときは「なんで??」という感じだったけど、Deferred で考えるとすんなり理解できる。

理想的にはすべての操作を関数で書きたいんだけど、現実にはそれが無理なので (非同期 API というものが存在するので)、汚い部分を全部 Deferred モナドに任せてしまって、本質の部分は綺麗な関数だけでやろうというのがモナドなんだと思う。

図にするとこんな感じかな。

f:id:edvakf:20100308012107p:image

Deferred を使って非同期処理をする場合、一つ一つの小さな処理は (普通の世界の) 単なる関数として書けるけれど、全体としては Deferred の世界から抜けることはできないという意味。

Haskell のモナドの場合は、図の下向きの矢印 (モナドでラップされた値を取り出す) は bind の中で担当してくれて、それに続く右向きと上向きの矢印までを関数として与えなければいけないけど、Deferred モナドの場合は右向きの矢印の関数を与えてあげると単純に return を継ぎ足してくれるのでそういうところは心配しなくても大丈夫というわけ。


結論 (jQueryはモナドだの受け売り)

  1. モナドは秘術的で難解な計算機科学ではない - 便利なものだ
  2. あなたはおそらく気がつかないうちにモナドを使ったことがあるだろう
  3. Deferred は素晴らしい
トラックバック - http://javascript.g.hatena.ne.jp/edvakf/20100306
 |