このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

JavaScriptによるテンプレート・モナド、すっげー簡単!

先週書いたエントリー「圏論やモナドが、どうして文書処理や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];}
}
  1. 括弧('{'と'}')のエスケープ処理はサボりますダハハハハハ)。
  2. 関数processTemplateは、テンプレート展開処理を行います。第1引数は、構文的に正しいテンプレート・テキストだと仮定しています(そうでないとうまく動かない)。'{'が先頭に来ても(少なくともRhinoでは)これで大丈夫なようです。
  3. コンテキストとは、文字列引数(名前、キー)を1つ取る関数のことだとします。
  4. 関数processContextは、2つのコンテキスト(コンテキストは関数ですよ!)を引数として、「第1のコンテキストcon1の値(展開テキスト)を、第2のコンテキストcon2で展開した値を返すコンテキスト」を返します。
  5. 関数contextFunは、マップ(JavaScriptオブジェクト)データで与えられたコンテキストを、関数としてのコンテキストに直します。contextFunは必ずしも必要なものではありませんが、あれば便利です。

[追記] これは余りにも手抜きだと思う方は、クワタさんによるPython版を参考にしてみてください→http://return0.dyndns.org/d/2007/01/26http://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つの法則は、それぞれ左単位法則と右単位法則になるように仕組まれている。つまり、ごくごく普通の、ものすごく当たり前の、中学生でも知っている計算法則に過ぎないのですよ、モナド法則ってのは。

とはいえ、このスッキリとした事実にたどり着くには、紙と鉛筆でラムダ計算を実行できることは必要だな、やっぱり。