Inverting back the inversion of control or, Continuations versus page-centric programming, Christian Queinnec, (2)

いまさらですが、前回の続き。

本題の、継続を使ってどのようにクライアントの状態を楽に保存するか、という話です。
本論文に書いてある簡単な例を見てみましょう。二つの値を入力させて、それらの和を求めるようなwebアプリケーションです。


(define n1 0)
(set! n1 (read-first-number))
(display-result-page n1 (read-second-number n1))

read-first-numberは一つ目の値を入力するためのフォームを生成し、そのフォームに入力された値を返す関数、read-second-numberは二つ目の値を入力するためのフォームを生成し、そのフォームに入力された値を返す関数だとします(ここで引数としてn1を渡しているのは、一つ目の値として入力された値を、二つ目の値を入力するページで表示するために使っているだけのようです)。そして、display-result-pageという関数は、二つの値を受け取って、それらの和を表示するための関数です。
前も書いたようにある式の継続とは、「その式が実行された後の環境における、その後の残りの計算」を表しています。よって、read-first-numberを呼んだ後の継続というのは、以下のような形になります。


(set! n1 ???)
(display-result-page n1 (read-second-number n1))

ここで、???はread-first-numberの戻り値を表しています。仮にread-first-numberの戻り値として123が帰ってきた場合、read-second-numberの呼出し後の継続は


(display-result-page n1 ???) | n1 = 123

ここで、|より右の部分は、その時の環境を表しています。Schemeでは、継続はラムダ記法で


(lambda (n2) (display-result-page n1 n2)) | n1 = 123

と表現されます。このように状態を持った関数の事をクロージャといいます。
ここまでくると、だんだんわかってくると思いますが、この論文では、ユーザからの入力が必要となるようなフォームを表示する場合、そのページを出力するだけでなく、その時点での継続を保存します。そして、submitボタン(もしくはリンク)にその継続を呼び出すようなURLを結びつける、ということをやっています。
そうすると、継続の中にそれまでの状態とこれから行うべき処理が含まれているので、セッションにデータを入れたりする必要が無くなる、というわけなのです。

そして、read-second-numberはSchemeでは以下のように実装することが出来ます。


(define (read-second-number n1)
(define (html-generator kUrl)
(html (head (title "SumServlet")
(body (form method: 'post action: kUrl
(table (tr (td) (td n1))
(tr (td "+") (td (input type: 'text size: 10 name: "n2"))))
(input type: 'submit
value: "SUM?" )))))
(let ((httpRequest (show html-generator)))
(string->number (get-request-parameter httpRequest "n2"))))
ここでキモになるのがshowという関数です。


(define (show html-generator)
(call/cc
(lambda (k)
(display (html-generator (k->url k))
(current-connection))
(close-output-port (current-connection))
(suicide))))
call/ccは、呼ばれた時点での継続をkに渡します。そして、k->urlという関数は継続をURLに変換するための関数とします。そうすると、URLとしてshowが呼ばれた時点での継続がURLの形式で渡され、そのURLがフォームを送信する先となるのです。
こうしてしまえば、サーバはこのコネクションをきってしまいます。
そして最後にリクエストを受け取るサーバプロセスを定義します。ここでは簡単のため、一つのスレッド上で動くとしています。

(define (server httpRequest)
(let ((url (get-request-url httpRequest)))
(let ((k (url->k url)))
(if k
(k httpRequest)
(error "lost continuation" url)))))
ここでは、リクエストのURLを見て、それに対応する継続が存在すればその継続を実行し、無ければエラーを出力しているだけです。

showやserverといった関数は汎用的なものなので、アプリケーションの開発者はアプリケーションの全体の流れ(一番最初のプログラム)と、その中でフォームを生成する関数を定義してあげれば良い、ということになります。
こうすることで、クライアントの状態を監視するのに面倒なセッション管理を行う必要が無くなります。

本論文には、他にも一回だけしかsubmitしたくない場合も簡単に実装できるよ、とかフォームの結果を検証するのも簡単に出来るよ、とか面白い話が書いてあります。

この論文、書いてある内容もわかりやすいのだが、継続の説明がかなりわかりやすい気がする。沢山例を使っているので、実感として継続が何のためのものなのか、わかりやすい。
なので、継続を勉強したいとか言う場合にも読むといいんじゃないかな、と思ったりした.