いままでClojureの紹介記事書こうかと思ってたんですが、でもClojureって結構本も出てるから、紹介記事なんかもう必要ないんだよなと気がついたので、Clojureをどう使ってるかを書いていったほうがいいかなと思いました。Shelfmap の開発でそれなりに長い間ClojureでWebアプリケーションを作っているので、そこで作ったものや、得たノウハウを共有するのは、Clojureコミュニティの役に立つかもしれないし、なにか改善点があれば教えてもらえる可能性もありますし。

ClojureでWebアプリケーションを書くと、サーバーでのURLルーティングはRing仕様に則って作るのが普通です。実装としてはCompojure (https://github.com/weavejester/compojure) が一番メジャーだと思う。

Compojureのリクエスト・ハンドラは、リクエストMapを引数にとって、レスポンスMapを返す、単純な関数です。単純な関数なのでテストも書きやすく、関数として独立しているし、引数も戻り値も単なるマップなので、単純な関数のテストとして書けます。これだけ単純だと、「書き方」も何もないと思いがちですが、リクエスト・ハンドラがやるべきことは多岐にわたります。

要求されたContent-typeに対応しているか。application/jsonを要求されたけどハンドラがtext/htmlにしか対応していないなら、HTTPステータスコード406 Not Acceptableを返すべき

ログインしているか？ してないなら401 Unauthorizedを返す

一時トークンは存在するか？存在するなら、正しいトークンか？

パラメータは必要なものがそろっているか

指定されたリソースは存在するか？

などなど、いろんな共通処理があって、これらをなるべく完結に、統一された構文で書きたい、という気持ちになるわけです。

そこでShelfmapでは、関数をスレッドマクロ風にチェインさせることで、リクエスト処理で共通の処理を統一的な記述方法で書けるように工夫しました。

コンテキスト・ハンドラ

Shelfmapでのリクエスト・ハンドラは、すべて以下のような形で書かれています。

(defn count-user-path [path-id-raw] (let [handler (chain (check-media-types "application/edn") (check-parameters path-id-validator {:path-id path-id-raw}) :let [path-id (parse-long path-id-raw)] (check-exists (fn [ctx] (path-exists? path-id))) (handle-count-user-path path-id))] (run-handler handler)))

chain マクロの内側には、「contextマップを引数にとり、新しいcontextマップあるいはringレスポンスマップを返す関数」をずらずらと書くことができます。contextマップとは、以下のような構造のマップです。

{ :request Ringリクエストマップ }

:request キーに、リクエストマップが結びついているマップです。Ringリクエストマップを直接使わないのは、ハンドラが新しいコンテキストを返すときに、Ringリクエストマップの既存のキーを誤って書き換えてしまう、という可能性について、いちいち考慮するのが、心理的に面倒くさく感じられたからです。このようにラップして、書き換えるのはあくまでもcontextマップだという形にすれば、誤ってRingリクエストマップを書き換えてしまう可能性がほぼ無くなりますし、逆に、書き換える意思があるのであれば、いつでも update 関数で書き換えることができます。

chain マクロは、contextハンドラを先頭から処理し、その結果がRingレスポンスマップであれば、即座にそのレスポンスを返却し、以降のcontextハンドラを呼び出しません。レスポンスマップでなければ、戻り値は新しいcontextマップであると解釈し、次のcontextハンドラを呼び出します。これを繰り返すのみです。 chain 内の最後のcontextハンドラが、このハンドラが本来やりたかったタスクを実行して、レスポンスを返すことになります。

:let ブロック

chain マクロの途中に、contextハンドラ関数以外に、 :let というキーワードを置くこともできます。 :let は、次のcontextハンドラを呼ぶ為には別の関数の結果が必要であったり、前のcontextハンドラが作ったcontextマップから、値を取り出す必要があるときに使います。その名の通り、Clojureの let マクロと同じ動作をするよう、設計しています。

:let [{{{source-raw :source, position :position} :params, session :shelfmap-session} :request} % (check-exists (path-info session source position))

このコードはあるchain処理の一部を抜き出したものです。 :let 部では特殊な値として % を使えます。 % には、常に最新のcontextマップが束縛されています。このコードでは、最新のcontextマップから（分配束縛を使って） session オブジェクトと、いくつかのパラメータを取得し、それらを次のハンドラの引数として使っています。

contextハンドラ関数の共通化

check-media-types や check-parameters は高階関数で、 check-media-types であれば、リクエストのacceptヘッダに、引数で指定したタイプが入っていなければ、 406 Not Acceptable レスポンスを返します。それ以外の場合は、元のcontextマップをそのまま返します。

check-parameters は引数にバリデータ関数を受け取り、リクエストパラメータを検証し、エラーがあれば 400 Bad Request を返します（エラーでなければ、やはり元のcontextマップを返します）。

これら二つの関数では行ってませんが、contextハンドラはcontextマップに新しいキーを追加することで、次のハンドラに値を渡すこともできます。

仕組みとしてはこれだけの単純なものですが、すべてのリクエスト・ハンドラを統一の方法で書けることでハンドラ関数の再利用性が高まり、コードが書きやすくなりました。

残念ながらこれらのコードは、現在はアプリケーション内のネームスペースに定義されていて、ライブラリとして分離していない為、ここだけを外部公開することができない状態ですが、私が書く他のコードでも同じ仕組みは必要になるので、そのうちに外部化しようと考えてます。