先週書いたエントリー「圏論やモナドが、どうして文書処理やXMLと関係するのですか?」の内容を実際に確認するためのJavaScriptプログラムを書いてみました。
3つの関数を含み、全部で12行のライブラリです。
/* templ-process.js */ function processTemplate(templ, con) { var a = (templ.replace(/\}/g, '{')).split('{'); for (var i = 0; i < a.length; i++) if (i%2 == 1) a[i] = con(a[i]); // コンテキストconは関数 return a.join(''); } function processContext(con1, con2) { return function (k) {return processTemplate(con1(k), con2);} } function contextFun(map) { return function (k) {return map[k];} }
- 括弧('{'と'}')のエスケープ処理はサボります(ダハハハハハ)。
- 関数processTemplateは、テンプレート展開処理を行います。第1引数は、構文的に正しいテンプレート・テキストだと仮定しています(そうでないとうまく動かない)。'{'が先頭に来ても(少なくともRhinoでは)これで大丈夫なようです。
- コンテキストとは、文字列引数(名前、キー)を1つ取る関数のことだとします。
- 関数processContextは、2つのコンテキスト(コンテキストは関数ですよ!)を引数として、「第1のコンテキストcon1の値(展開テキスト)を、第2のコンテキストcon2で展開した値を返すコンテキスト」を返します。
- 関数contextFunは、マップ(JavaScriptオブジェクト)データで与えられたコンテキストを、関数としてのコンテキストに直します。contextFunは必ずしも必要なものではありませんが、あれば便利です。
[追記] これは余りにも手抜きだと思う方は、クワタさんによるPython版を参考にしてみてください→http://return0.dyndns.org/d/2007/01/26、http://return0.dyndns.org/d/2007/01/30 [/追記]
次は、テストのセットアップをするものです。
/* templ-test.js */ var message = "{greeting}\n{body}\n--\n{sign}\n"; var condata1 = { greeting:"Hello, {person}.", body:"It's a {good-or-bad} News, ...", sign:"Hanako" }; var condata2 = { person:"Tonkichi", "good-or-bad":"Good" }; var confun1 = contextFun(condata1); var confun2 = contextFun(condata2);
JavaScriptインタープリタRhinoで日本語の表示がおかしくなるので、例文を英数字で書いています。そのRhinoで実行してみると:
js> load("templ-process.js") js> load("templ-test.js") js> processTemplate(processTemplate(message, confun1), confun2) Hello, Tonkichi. It's a Good News, ... -- Hanako js> processTemplate(message, processContext(confun1, confun2)); Hello, Tonkichi. It's a Good News, ... -- Hanako js>
これは、「圏論やモナドが、どうして文書処理やXMLと関係するのですか?」の山場である「多段階のテンプレート処理」に出てきた等式を確認した例です。同様のことをブラウザでやるには、例えば次のようなHTMLファイルを準備してください。
<!-- templ-test.html --> <html> <head> <script src="templ-process.js" ></script> <script src="templ-test.js" ></script> <script> function test1() { alert( processTemplate(processTemplate(message, confun1), confun2) ); } function test2() { alert( processTemplate(message, processContext(confun1, confun2)) ); } </script> </head> <body> <h1>Template processing test</h1> <ol> <li><button onclick="test1();" >Test 1</button> <li><button onclick="test2();" >Test 2</button> </ol> </body> </html>
「圏論やモナドが、どうして文書処理やXMLと関係するのですか?」の「モナドに向かって突っ走れ!!」と「バッチリ、モナドだぜぇ」で説明した、テンプレート・モナドのextとunit(それぞれ、モナドの拡張と単位を与える)も、次のように簡単です。
function ext(con) { return function (t) {return processTemplate(t, con);} } function unit(k) { return "{" + k + "}"; }
これらの素材があれば、モナド法則を具体例で実験できます。
js> unit("foo") {foo} js> (ext(unit))("{foo}bar") {foo}bar js> (ext(confun1))(unit("greeting")) Hello, {person}. js> confun1("greeting") Hello, {person}. js>
モナド法則の3番目を具体例で確認するのは少しだけ面倒ですが、良い練習でしょう。(分からなかったら、ココを見てください。)
再チャレンジ支援・考え方のヒント
「圏論やモナドが、どうして文書処理やXMLと関係するのですか?」で、挫折しがちな箇所は、まず、コンテキストをデータと考えたり関数と考えたりするところでしょう。processTemplateが前もってあるとして、processContextを二通りに書いてみます。
// コンテキストがデータの場合 function processContext(condata1, condata2) { var result = {}; for (var key in condata1) { result[key] = processTemplate(condata1[key], condata2); } return result; }
// コンテキストが関数の場合 function processContext(confun1, confun2) { return function (key) {return processTemplate(confun1(key), confun2);} }
いずれの場合も、第1コンテキストの展開テキスト(condata1[key]またはconfun1(key))を第2コンテキストにより展開しています。この展開処理をいつどのタイミングで行うかが違ってますね。展開処理結果を保持/再利用するか捨てて毎回やり直すかも違います。が、概念レベルで考えれば同じことなんです。
それと、モナド法則「ext((ext(con2))・con1) = ext(con2)・ext(con1) 」が唐突で天下り、イミフメイと感じるでしょう。extの定義 (ext(con))(t) := processTemplate(t, con) に戻って、「・」もラムダ計算を使って書き換えると:
- processTemplate(t, processContext(con1, con2)) = processTemplate(processTemplate(t, con1), con2)
さらにラムダ計算をして:
processContext(processContext(con1, con2), con3) = processContext(λk.(processTemplate(con1(k), con2)), con3) = λj.processTemplate(λk.(processTemplate(con1(k), con2))(j), con3) = λj.processTemplate(processTemplate(con1(j), con2)), con3) ここで、t = con1(j) だと思って先の等式を適用(右辺→左辺と変形) = λj.processTemplate(con1(j), processContext(con2, con3)) = processContext(con1, processContext(con2, con3))
結局、processContext(processContext(con1, con2), con3) = processContext(con1, processContext(con2, con3)) なのですが、processContext(X, Y)という関数呼び出し形式を X※Y という二項演算形式にしてみると、
- (con1※con2)※con3 = con1※(con2※con3)
これって、※に関する結合法則ですね。そう、モナド法則の3番目は、実際上は結合法則。普通よく目にするモナド法則は、結合法則のモトになるように最初から仕組まれている形なわけよ。残りの2つの法則は、それぞれ左単位法則と右単位法則になるように仕組まれている。つまり、ごくごく普通の、ものすごく当たり前の、中学生でも知っている計算法則に過ぎないのですよ、モナド法則ってのは。
とはいえ、このスッキリとした事実にたどり着くには、紙と鉛筆でラムダ計算を実行できることは必要だな、やっぱり。