2010年1月10日日曜日

Advanced Control Flow

暫くの間、ブログがたった一つしか新規投稿を受け付けない、と言う明らかに大きな問題は無視しましょう。心配しないで!これは後で直します。

しかしながら、私たちのプログラムにはもっと高度な問題があります:と言うのも、startはアプリのURLにリクエストを送る機能なんですが、あまりにも多くの役割がありすぎて負荷がかかってきています。概念的には、startは現時点、二つの種類のリクエストを処理しています:ブログを表示するリクエストとブログへの新規投稿を加えるリクエストと、です。

一体、startが、私たちのWebアプリの全ての振る舞いを、お巡りさんのように --- dispatcherと呼びますが --- 交通整理をするようになったらどうなるのでしょうか?想像してみると、アプリに機能を追加するたび、startは制御方法を知らなくてはなりません。果たして、違う種類のリクエストがある度に、自動的に違う機能へと振り分ける方法なんてあるんでしょうか?

web serber ライブラリにはURLを生成して、アプリの別々のパーツへと振り分けるsend/suspend/dispatchと言う機能が用意されています。素敵なデモを紹介しましょう。新規ファイルを立ち上げて、以下のコードを定義ウィンドウに入力してください。


#lang web-server/insta
; start: request -> html-response
(define (start request)
(phase-1 request))

; phase-1: request -> html-response
(define (phase-1 request)
(local [(define (response-generator embed/url)
`(html
(body (h1 "Phase 1")
(a ((href ,(embed/url phase-2)))
"click me!"))))]
(send/suspend/dispatch response-generator)))

; phase-2: request -> html-response
(define (phase-2 request)
(local [(define (response-generator embed/url)
`(html
(body (h1 "Phase 2")
(a ((href ,(embed/url phase-1)))
"click me!"))))]
(send/suspend/dispatch response-generator)))

これはグルグル回るwebアプリです。ユーザがアプリを最初に訪れると、phase-1が始まります。そのページはハイパーリンクを生成し、クリックするとphase-2へ飛びます。ユーザがクリックするとまたphase-1へと戻り、これが延々と繰り返されます。




もうちょっと丁寧にsend/suspend/dispatchのメカニズムを見てみましょう。send/suspend/dispatchはレスポンス生成機能を受け取り、そして特殊なURLを作るembed/urlと呼ばれるレスポンス生成機能を返します。このURLが特殊なのは次のような意味です:ウェブブラウザがこれらのURLを訪ねると、webアプリが再起動しますが、それはstartからではなく、このURLに関連したハンドラから立ち上がる、と言う意味です。Phase-1ではembed/urlの用途はPhase-2に関連してて、逆もまた同じ、と言う事です。

embed/url絡みのハンドラをもっと綺麗にしてみましょう。ハンドラは単にリクエストを受け取る機能なので、localで定義可能です。結局、ローカル定義のハンドラは定義のスコープ内に存在する全ての変数を捕捉します。もう一つ、ループ的な例を見てみます。


#lang web-server/insta
; start: request -> html-response
(define (start request)
(show-counter 0 request))

; show-counter: number request -> html-response
; ハイパーリンクで数値を表示し、リンクがクリックされると
; 数値が増えた新しいページを生成する
(define (show-counter n request)
(local [(define (response-generator embed/url)
`(html (head (title "Counting example"))
(body
(a ((href ,(embed/url next-number-handler)))
,(number->string n)))))

(define (next-number-handler request)
(show-counter (+ n 1) request))]

(send/suspend/dispatch response-generator)))

この例は、対話的結果が累積可能である、と言う事を示しています。ユーザがページを訪問し、ページを生成してゼロを目にしたとしても、ハンドラは対話を介して次の数値へ続くハンドラを生成し、値はどんどん累積されていくのです。





ちょっと寄り道し過ぎたので、ブログアプリへと戻りましょう。フォームの動作を別のハンドラに関連したURLと結びつけるように調整します。


#lang web-server/insta

; ブログは (listof post) で
; 投稿は (make-post title body)
(define-struct post (title body))

; BLOG: blog
; 静的ブログ
(define BLOG
(list (make-post "First Post" "This is my first post")
(make-post "Second Post" "This is another post")))

; start: request -> html-response
; リクエストを受け取り、全てのwebコンテンツを
; 表示するページを生成する
(define (start request)
(render-blog-page BLOG request))

; parse-post: bindings -> post
; 束縛から投稿を抽出する
(define (parse-post bindings)
(make-post (extract-binding/single 'title bindings)
(extract-binding/single 'body bindings)))

; render-blog-page: blog request -> html-response
; ブログとリクエストを受け取り、ブログのコンテンツの
; html-responseを生成する
(define (render-blog-page a-blog request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog)
(form ((action
,(make-url insert-post-handler)))
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))

(define (insert-post-handler request)
(render-blog-page
(cons (parse-post (request-bindings request))
a-blog)
request))]

(send/suspend/dispatch response-generator)))

; render-post: post -> html-response
; 投稿を受け取り、投稿の要素のhtml-responseを生成する
(define (render-post a-post)
`(div ((class "post"))
,(post-title a-post)
(p ,(post-body a-post))))

; render-posts: blog -> html-response
; ブログを受け取り、全投稿の要素の
; html-responseを生成する
(define (render-posts a-blog)
`(div ((class "posts"))
,@(map render-post a-blog)))

render-blog-page機能の構造は、前のshow-counterの例と極めて似ているところに注目してください。最終的に、ユーザはブログに複数の投稿が可能になり、それを読む事が出来るようになりました。

残念ながら、まだ問題があります。問題を見るには次のようにしてみてください:システムにいくつか投稿して、新しいウィンドウでブラウザを開きます。新しいブラウザでwebアプリのURLを開きます。一体何が起きるでしょう?

0 件のコメント:

コメントを投稿