北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

jQueryは本当にモナドだった

タイトルはかなり釣りです:-) まあ、間違えているかもしれないので内容の判断は自己責任で。

さて、元ネタはこちらなのですが、独自のモナド節を唱えていて非常に怪しい。と言うことで、怪しくならないように真面目に解説してみます。

ちなみに、このエントリでは数学のモナドではなくHaskellMonad、つまりKleisli Tripleとして説明します。

元記事のどこが怪しいのか

元記事では「流れるインタフェース」がbindであるかのように書いていますが、ここが怪しいと感じます。「流れるインタフェース」は単なる関数合成(haskelでいうg . f)であり、クライスリ結合ではないと思います。後述するように、bindが提供するクライスリ結合は、違う型同士をくっつけられるという奇妙な性質を持つ合成です。

元となる圏

DOMエレメントとjQueryオブジェクトのみを含む任意の集合を対象(Object)、その間の関数を射(Arrow)と見なすのが一番楽でしょう。他の型はjQueryでの扱いがよくわからないので省きます。

型構築子, return, bind

型構築子としては、$()を使います。すごい平たく言えば、jQuery型、と言ってもいいでしょう。が、JSの型はいい加減なので、型と言うよりは値の任意の集合に$()を適用してできた値の集合、のようなイメージになります。また、$( $() )は$()となります(というふうにjQueryが実装されてます)。これは、Tをモナド(関手)とすると、 T T X == T X を満たす、つまり二回自己関手をかけると潰れるということです。

returnは、 $() となります。フラットな値を jQueryでラップした値に変換してくれます。

bind に関しては、 .map を使いたいのですが、jQuery1.4の .map の実装で試してみたところ、 jQueryオブジェクトを返す関数だとうまく扱えないようです*1。なので、しゃあないので自分で用意します*2

    $.fn.extend({
        mBind: function (fn) {
            return $( $.map(this, function( elem ) {
                return fn.call(elem, elem).get();
            } ) );
        }
    });

やってることは簡単で、「なんらかの値(DOM or jQueryオブジェクト)を受け取ってjQueryオブジェクトを返す関数」を、"自然な感じ"でjQueryオブジェクトに適用します。自然な感じとはモナド則を満たすってことです。

モナド

モナド則の return と >>= を $() と .mBind() で書き換えて、本当に一緒になるかを見ます。QUnitで書くとこんな感じです。

ただし、jQueryオブジェクトはDOM要素だけでなく履歴も持っている*3ようなので、このmBindでは厳密にはモナド測を満たさないかもしれません。ここでは、DOMをラップするって性質に関してはモナドになるだろう、程度の解説に留めておきます。

考察1: mBindで合成(クライスリ結合)できる関数

ここがモナドのポイントとなる部分ですが、「普通の値を受け取ってjQueryオブジェクトを受け取る関数( DOM -> $() )」をどんどん合成できます。上のgistで用意したfは、「DOMエレメントの下にある<span>要素を表すjQueryオブジェクトを返す関数」で、gは「DOMエレメントの親を表すjQueryオブジェクトを返す関数」となります。このようなfとgについて、mBind(f).mBind(g) という合成ができます。mBindが面白いのは、DOMと$() という違う型同士の関数を合成できるという点です。*4

また、「$( $() ) なる型の値に $() -> $() なる関数を適用」と言う適用パターンも当然ありです。ただし、$( $() ) はjQueryの実装上 $() となるので、 $() に $() → $() なる関数を適用できるということしか言ってないので全然面白くないです。

考察2: μを求めてみる

数学のモナドでは、関手Tと自然変換η:ID→T、μ:TT→Tでモナドを定義します。Tは.map()関数*5、ηはreturnなのでいいとして、この場合のμはどうなるでしょうか?

実はμの出し方はこちらにあるようにそんなに難しくないです。先ほどのリンクのjoinがμなので、これを元にちょっと書き下してみましょう。

var id = functoin (x) { return x; };
var join = function (jq) { return jq.mBind( id ); };
/*
        => function (jq) {
               return $( $.map(jq, function( elem ) {
                   return id.call(elem, elem).get();
               } ) );
           }
        => function (jq) {
               return $( $.map(jq, function( elem ) { return elem.get(); } ) );
           }
        => function (jq) { return jq; }
        => id
*/

ということで、μは恒等関数になることがわかりました。これは $( $() )と$()が同じことから、直感的ですね。

*1:これは、 $([ $("#id") ]) という書き方が出来ないことに起因していると思われます

*2:これと同じ動きをする関数をjQueryが持っているようなら教えて下さい:-p

*3:.andSelf() とか .end()

*4:元記事で紹介されていた .fedeout().text() の合成は、どちらも$() → $() の関数をつなげてるだけなので、モナドのクライスリ結合(bind)じゃなくただの関数の合成です。

*5:厳密には前述したように適用できない関数もあるから駄目ですが。