Weblocksの継続ベースの画面遷移

WeblocksCommon Lisp用のWebアプリケーションフレームワーク(WAF)。Common LispのWAFも色々出てきているみたいだけど、一番リッチに作り込まれているのはこのWeblocksだと思う。なにしろ1行のHTMLもJavascriptも書かずにWebアプリを作ることができるという触れ込みだ。一方でドキュメントが少なくかつ古いので分からないところはソースコードをあたるしかなかったりもする。インストールはquicklisp一発で済むので昔に比べればはるかに簡単になった。

;; インストール
(ql:quickload :weblocks)

;; スケルトン生成 (この場合 /home/coffeemug/projects/myapp というディレクトリができる)
(wop:make-app 'myapp "/home/coffeemug/projects/")

WeblocksはWebアプリをウィジェットの木として作るという考え方に基づいている。ウィジェットはCLOSオブジェクトで、最終的にHTML+Javascriptに展開されてブラウザに表示される。そのため、ウィジェットをどのように展開するかを記述するrender-widget-bodyというメソッドを持っている。ウィジェットは最初から色々そろっているが、もちろん自分で定義することもできる。

Weblocksは継続ベースの画面遷移ができるというところがミソらしい(参考 http://d.hatena.ne.jp/inuzini-jiro/20110310/1299735824)。普通のWAFではサーバサイドでセッションオブジェクトを保存し、ユーザはサーバとのやりとりの度にセッションIDを伝えて現在の状態を引き出さなければならない。一方で継続ベースの場合はセッションごとに継続という「それ以降の計算処理が保存された関数オブジェクト」を持っていて、同じユーザから再びリクエストがあったら前の継続の続きを再生する。プログラマとしてはセッションにまたがる変数をあたかも普通の局所変数のように扱えるので楽になる。Ajaxと継続を組み合わせることで、一部のウィジェットだけを置き換えたりもできる。

以下のコードはAjaxウィジェットの置き換えを行う。

(defun with-flow-test-page ()
  (let* ((comp-widget (make-instance 'composite))
	 (meta-comp-widget (make-instance 'composite :widgets (list comp-widget)))
	 (counter 0))
    (setf (composite-widgets comp-widget)
	  (list (make-widget (lambda () (with-html (:p (str (local-time:format-timestring nil (local-time:now))))
						   (:p "This is car of comp-widget"))))
		(make-widget
		 (lambda ()
		   (render-link
		    (lambda (&rest args)
		      (declare (ignore args))
		      ;; 第一引数はyieldの引数のウィジェットで置き換えられるウィジェット
		      (with-flow comp-widget
			(loop
			  (yield (make-widget
				  (lambda (cont) ; contが継続オブジェクト
				    (with-html (:p (str (local-time:format-timestring nil (local-time:now))))
					       (:p (str (format nil "with-flow loop 1st. counter=~A" counter)))
					       (render-link (lambda (&rest args)
							      (declare (ignore args))
							      (incf counter)
							      (answer cont)) ; with-flowにおける次の処理へ
							    "Goto 2nd"
							    :ajaxp t)))))
			  (yield (make-widget
				  (lambda (cont)
				    (with-html (:p (str (local-time:format-timestring nil (local-time:now))))
					       (:p (str (format nil "with-flow loop 2nd. counter=~A" counter)))
					       (render-link (lambda (&rest args)
							      (declare (ignore args))
							      (incf counter)
							      (answer cont))
							    "Goto 1st"
							    :ajaxp t))))))))
		    "replace comp-widget")))))
    (render-widget meta-comp-widget)))

(defun init-user-session (root)
  (setf (widget-children root)
	(list (make-widget
	       (lambda ()
		 (with-html(:p (str (local-time:format-timestring nil (local-time:now)))))))
	      #'with-flow-test-page)))

このWebアプリを実行してみるとこのような画面になる。

最初ウィジェットの構造はこうなっている。meta-comp-widgetやcomp-widgetウィジェットを格納するだけのcompositeウィジェット。"replace comp-widget"と表示されているリンクを押すと、with-flowマクロが呼び出されて、with-flowマクロの第一引数であるウィジェット、この場合comp-widgetが置き換えられる。

その結果がこの画面。

先ほどリンクを押したことでcomp-widgetがwith-flowマクロの中のyield以下のウィジェットに置き換わったのだ。新しいウィジェットにもリンクがあり、これを押すことでwith-flowマクロの中の2番目のyield以下のウィジェットにさらに置き換わる。

その時の画面がこれ。

二つのタイムスタンプが表示されているが、meta-comp-widgetに入っているタイムスタンプは最初から変わらない。一方、その下の次々に置き換わっていくウィジェットにより表示されるタイムスタンプは置き換えの度に更新されることが分かる。しかも、with-flowの本体部分ではloopを入れているのでリンクを押す度にページの一部分がトグルするようになっている。loopに限らず、任意のLispの制御構造を入れられるので、様々な条件に合わせてウィジェットを表示したりできる。
また、counterをページ遷移の度にインクリメントしているが、局所変数のように書いているにも関わらずページをまたいでも状態を保っていて、セッションオブジェクトみたいなものを明示的に用意する必要がない。これが継続ベースのWebアプリのメリットなのかな、と思う。他のWAF使ったことないけど・・・