Diary?

2009-10-07
Wed

(14:30)

ええまあまったくその通りで、ユーザ定義のメタコマンドを書かせるとなると、本気大マジでモナドを理解してもらう必要があり、さらにはメタコマンド同士の結合がモナド則を満たしていないと実行時に悲劇が起こる(あるいは実行すらできない)ので、こりゃまずいなと思ったわけです。

まず今の仕様の構文がこれ。

logged_in | has_token | is_valid | process_form | print /form/ok.html

エラー処理は全部自動(パイプラインを中断させる例外を発生させる)なので、これは使い物にならない。というわけでタグディスパッチでエラー処理を書く(previouse.html が元の画面とする)。

logged_in | when {
    OK => has_token | when { 
        OK => is_valid | when {
            OK => process_form | print /form/ok.html,
            NG => print /previouse.html
        }, 
        NG => print /previouse.html
    },
    NG => print /login.html
}

when 式の } の後にカンマを忘れそうというのを除けばまあ悪くはないかなあと思いつつ、俺が提案したのはこれ。

logged_in | has_token | is_valid {
    process_form
} | when {
    OK => print /form/ok.html,
    INVALID => print /previouse.html,
    DUP_TOKEN => print /previouse.html,
    NO_TOKEN => print /previouse.html,
    NOT_LOGGED_IN => print /login.html
}

最後にエラー処理をまとめて書いた方が、色々精神衛生にいいじゃん。何よりこっちの方が読み易いし。

ただし、これを実現するにはコマンドを受け取ってコマンドを返すメタコマンドが必要で、そのメタコマンドはいわゆる関手であり、さらにはメタコマンドによって生成されるコマンドの型がモナド則を満たさないと使い物にならないことが判明。実際に使うのは Maybe モナドのバリアントみたいなものなのだが、仮にそれを Exceptional 型として、 Caty の JSON スキーマの構文風に書き下すと

type Exceptional = @OK _T | @INVALID | @NO_TOKEN | @DUP_TOKEN | @NOT_LOGGED_IN;

任意の型に OK タグを付けたものとそれ以外のタグが付いているもののユニオンとなる。実際にユーザ定義の Exceptional 型を使う関手を認めるなら、さらにこの型自体が拡張できないと不便なのだが、それは今のところ置いておく。

一旦先のパイプラインに戻る。 "logged_in | has_token | is_valid {process_form}" というメタコマンドの合成をどう捉えるかだが、これは次のパイプラインと意味的には同じ。ここでの pass は入力をそのまま出力にコピーするだけのコマンド。

logged_in {pass} | has_token {pass} | is_valid {process_form}

この時の各コマンドの型は process_form の入出力の型を X, Y とおけば

logged_in {pass} :: X -> @OK X | @NOT_LOGGED_IN
has_token {pass} :: X -> @OK X | @NO_TOKEN | @DUP_TOKEN
is_valid {process_form} :: X -> @OK Y | @INVALID

となり、普通に考えると型が違うのでコマンドを繋げられない。そこでモナドを(無理やり)導入して、 Haskell 風に書くなら

return a = @OK a
(@OK x) >>= f = f x
@NOT_LOGGED_IN >>= f = @NOT_LOGGED_IN
    :
    :

というモナド則を導入し、パターンマッチでコマンドの結合の行われているポイントで気合でディスパッチという事をすれば、先のようなコマンドラインを書く事ができる。

……が、いくらなんでもこれはハードコア過ぎる。もしもこれをやる場合、ただでさえコマンドとスキーマの宣言が必要なところへ

  • 関手の宣言
  • モナドの宣言

が入ってくるので、これはもう天地神明に誓って破綻しており、これだったら普通にタグディスパッチの入れ子を書いた方が一兆倍マシだし俺の労力も少なくなるというのが昨日のミーティングから戻る途中にいろいろ考えた挙句の結論。

もっとも開発の途中段階ではまず正常系だけ書いておき、エラー処理は後で書きたいみたいな要求も出てくるだろうし、そこで常にタグディスパッチしないと型エラーというのも(今は型エラーにはならないけど、静的チェック+型推論を導入するとエラーになる)どうかなと思わなくもない。いや、今は動的チェックしかしてないので正常系は普通に動くんだけどさ。

というわけでいろいろあって、ちと先のユーザ定義関手のアイディアは却下というかなんというか。ちなみに妥協案として考えていたものには次のようなものがあった。

try {
    logged_in | has_token | is_valid | process_form
} | when {
    OK => print /form/ok.html,
    INVALID => print /previouse.html,
    DUP_TOKEN => print /previouse.html,
    NO_TOKEN => print /previouse.html,
    NOT_LOGGED_IN => print /login.html
}

このやり方だと、先に存在した型エラーの問題は多分起きない。というのも try は関手ということなので、このパイプラインは

try {logged_in} | try {has_token} | try { is_valid } | try { process_form} | when ...

と等価で、 try 関手で移されたコマンドの結合さえどうにかすればいい。早い話が先に出したモナド則みたいなのを使ってコマンドを繋いでいけばいいわけで、これならタグディスパッチ方式と統合できるはず。一応補足的に書いておくと、 try 関手は

  • @OK タグの付いたデータはタグを剥ぎ取ってコマンドに渡す
  • 予め登録しておいたエラー用のタグの付いたデータは pass する
  • コマンドの出力値がタグ付きでなければ @OK を与える

という機能をコマンドに与える関手である。実はこれはこれで予めエラー用のタグを登録する方法が悩ましかったり、エラー用のタグではなく例外が投げられたらどうすんのかとかいろいろあるのだが、まあ logged_in とかを全部関手にしてユーザ定義関手を認めるよりは混乱は少ないかなと。

追記:いやまてよ、これだと関手を適用する前の時点で結合不能だ。ここは A|B 型を A -> X 型のコマンドに渡せるようにして、実行時エラーが出ることを認めるべきだな(実用上はそっちの方が良さげ)。つまりここでの try 関手は、実行時エラーの出る結合を実行時エラーの出ない結合に移す関手となる。

Creative Commons
この怪文書はクリエイティブ・コモンズ・ライセンスの元でライセンスされています。引用した文章など Kuwata Chikara に著作権のないものについては、それらの著作権保持者に帰属します。