八発白中

技術ブログ、改め雑記

Common LispのWebフレームワーク「Caveman2」を作りました

僕がClackを作ったのは2年9ヶ月前。Webフレームワークの「Caveman」をリリースしたのはその2ヶ月後だった。

Cavemanを作った頃、僕はアリエルネットワーク株式会社にいた。松山さんと開発していた試作プロダクトのために、並行して開発していたWebフレームワークがそれだった。そのときはCommon LispでのWebアプリケーション構築方法は洗練されておらず、アプリケーションを作りながら、手探りでフレームワークを作っていたことを今でも思い出す。

結局作っていた社内プロダクトは世に出なかったのだけど、それから1年後、会社が変わっても僕はCommon Lispでのプロダクトを作っていた。

ある日Cavemanを使っていたとき、一つのアイデアが浮かんできた。Cavemanは最初にプロジェクトのひな形を作る必要がある。けれど、PythonのFlaskのようなフレームワークではそれすら必要としない。Cavemanがその簡単さを得るには何が必要か。

その結果生まれたのが「ningle」だった。

Cavemanを“micro”フレームワークと呼んでいたので、さらに小さいningleは“super micro”フレームワークと呼んでいた。数日程度でCavemanからルーティング部分だけを抜き出しただけのningleは、プロトタイピングにはいいかもしれないけど、本番環境で使うほどのものとは思えず、僕はおもちゃ程度に考えていた。

結局今の会社で作ったCommon Lispの社内プロダクトも世に出ることはなく、僕はしばらくCommon Lispを使うのをやめ、PerlPythonJavaScriptの世界を転々と見て回った。

“micro” or “super micro”

そんな間でも、僕が作った2つのWebフレームワークはどちらもそれなりに使われているようで、ときどきメールで意見や質問をもらうことがあった。

質問の中でも、一番多かったのが、「Cavemanとningleというのがあるけど、どう違うの? どっちを使えばいい?」だった。

当時の僕にとってこれは自明だった。ningleはただのおもちゃで、使えるアプリを作るならCavemanじゃなきゃダメですよ。しかし、意外にもそう質問してくる人に限って選ぶのはningleだった。

あまりに意外だったので、その後僕は自分自身でningleを使ってみることに決め、Quickdocsをningleで作った。

使ってみて気づいたのだが、ningleはただ一つのことだけをやることに特化しており、見通しもよくドキュメントもほとんど参照する必要がないほど自明なWebフレームワークだった。

自画自賛のように聞こえるかもしれないが、偶然の結果産まれたプロダクトなので客観的な評価をするなら、非常に良くデザインされたWebフレームワークだった。

そこから改めて見ると、Cavemanは少し野暮ったく、無駄に制約を加えたように見え、それから僕が作るものはningleを使うことにしてきた。

“micro” shouldn't be an excuse

では、Cavemanの役割は終わったのか。Common LispのWebフレームワークの最善解はningleなのか。

ningleが好まれる一方で、Webフレームワークを求める人が口々にデータベース連携機能を求めるのに気づいていた。

軽量なフレームワークが欲しいのだけど、RESTASとCavemanどっちがいいだろう。データベースは扱える? いや、どっちもデータベース連携はサポートしていないよ。

仕事で扱うWebアプリケーションは大小問わずほとんどDBMSへの接続を行うのに、“micro”という名の元に実用的であるべきWebフレームワークがその機能を持たないのは、実装者の怠慢じゃないと言えるだろうか。

それと同時に運用にも問題があった。ningleのアプリケーション、具体的にはQuickdocsの運用で見つけた問題だが、エラーログはどこで設定し、どこにどういう形式で出力するのか。本番でのサーバ起動方法はどうすればいいか。ダウンタイム無しでのホットデプロイはどうすればいいか。どれも統一された方法は無かった。

もしそれらが全てのningleアプリケーションに必要なら、それをフレームワークが持たない理由は無い。

そうして僕は、ningleという確かな土台を得て、データベース連携機能を標準で持ち、Common LispのWebアプリケーションを運用する上でのベストプラクティスを備えたWebフレームワークを作るべきだと悟った。そしてその新しいフレームワークが、これまでのCavemanの役割を置き換えるのはとても自然な考えだった。

そうして出来たのが「Caveman2」だ。

Caveman2

いくつかCaveman1やningleと異なる点を中心にCaveman2の機能を紹介する。

Routing

前述の通り、Caveman2はningleをベースにしているが、ルーティング用のアノテーションを提供する点が違う。

@route GET "/"
(defun index ()
  "Hello, World!")

このリーダマクロで実現されたアノテーション記法を好んでいる人がいる一方で、まるでPythonのようで好かない、という人があまりに多くいることを知っているので、Caveman2ではdefrouteという普通のマクロも提供している。

(defroute index "/" ()
  "Hello, World!")

Caveman1 の @urlと異なり、引数リストがキーワードによる分配を行うところが違う。

;; Caveman1での従来の方法
@url GET "/hello/?:name?"
(defun say-hello (params)
  (format nil "Hello, ~A"
          (or (getf params :|name|)
              "Guest")))

;; Caveman2での方法
@route GET "/hello/?:name?"
(defun say-hello (&key name)
  (format nil "Hello, ~A"
          (or name "Guest")))

Caveman1やningleを使ったことのある方にはこの煩わしさは分かるだろうが、Caveman2ではコントローラで受け取ったパラメータからいちいちgetfdestructuring-bindで値を取り出したり、必要ないときに(declare (ignore params))など書かなくてもよくなっただけでなく、Common Lispでのキーワード引数という至極自然な方法で値を受け取ることができるようになった。もちろんデフォルト値も指定できる。

@route GET "/hello/?:name?"
(defun say-hello (&key (name "Guest"))
  (format nil "Hello, ~A" name))

Templating

デフォルトのテンプレートエンジンは変わらずCL-EMBを採用しているが、少し便利なマクロwith-layoutを追加した。

@route GET "/"
(defun index ()
  (with-layout (:title "Welcome to My site")
    (render #P"index.tmpl")))

“layout”はHTMLのヘッダやフッタなど、各ページで共通の部分を記述したものだ。CL-EMBにはこのような機能が無いため、Caveman2が独自で提供している。

Configuration

Caveman1では設定ファイルが分割されており、設定の部分共通化などがしづらく、全体の見通しが利きづらく、設定が煩雑になりがちだった。

Caveman2では設定機能をEnvyという新しいライブラリに分離した。

EnvyはPerlモジュールのConfig::ENVにインスパイアされており、Common Lispのライブラリとしては珍しく環境変数に依存する。環境変数の値によって設定が切り替わるので、サーバ起動部分が設定の橋渡しをする必要がなくなり、疎結合化が進んでシンプルになった。

(defpackage :myapp.config
  (:use :cl
        :envy))
(in-package :myapp.config)

(setf (config-env-var) "APP_ENV")

(defconfig :common
  `(:application-root ,(asdf:component-pathname (asdf:find-system :myapp))))

(defconfig |development|
  '(:debug T
    :databases
    ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"test.db"
                                                        *application-root*)))))

(defconfig |production|
  '(:databases
    ((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234")
     (:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234"))))

(defconfig |staging|
  `(:debug T
    ,@|production|))

Database integration

Caveman1にはない、まったくの新機能としてデータベース連携機能がある。

(use-package :caveman2.db)

(defun search-adults ()
  (let ((db (connect-db :maindb)))
    (select-all db :*
      (from :person)
      (where (:>= :age 20)))))

これはCL-DBISxQLを組み合わせたもので、コネクション管理まできっちりやってくれる。データベースが分割されたアプリケーションも考え、設定ファイルに複数のデータベース設定を書いたときに切り替えられるようにしている。

ningleのような確立されたものと違い、SxQLを使ったこの新しい機能はまだ善し悪しの評価がなされていない。今後変動の可能性があるとすればここだろう。

(Hot) Deployment

普段はREPLに引きこもっているLisperも、サーバへのデプロイとなるとシェルの力を借りる必要がある。

Caveman2ではShellyをサーバ起動 ツールとして採用している。

$ APP_ENV=development shly -Lclack clackup app.lisp --server :fcgi --port 8080

ワンライナーよりも簡単で、引数の受け取りもシェルコマンドの要領でできる。使う処理系を簡単に切り替えられるためデプロイに向いている。

Supervisordのような死活監視ツールを使う場合はこのコマンドを指定すればよい。

また、ダウンタイム無しでホットデプロイするには、PerlServer::Starterがそのまま使える。

$ APP_ENV=production start_server --port 8080 -- shly start --server :fcgi

Common Lispだけにとどまらず、さまざまな言語のツールを使う努力は、メインストリームではないコミュニティリソースの限られた言語には重要だと僕は思う。

Conclusion

まだQuicklispの反映が無いのでQuicklispから利用することはできないが、今月近いうちにリリースされると思う。その時はどうぞお試しください。

追記(11/12): Quicklispの反映があったため、利用可能になっています。(ql:quickload :caveman2) でお試しください。