Ringとnamespace-qualified keywordについて
これはClojure Advent Calendar 2017のために書かれた記事です。
とは言っても、そんなたいしたことを書くつもりは無くて、いつも書く記事の延長線上にある比較的実践的で軽いトピックです。本当は11月に書いて公開しようかなと思っていたのを、時期をずらして12月にしたっていうだけなので過度な期待はしないでください。
さて、というわけで今日はRingと名前空間付きキーワードについて、あるいはネストしたフォームの要素を名前空間付きキーワードを使って平滑化する話についてです。とりあえず、今回どういうプロジェクトを元に話すのか簡単に書いておきましょう。 プロジェクトの依存ライブラリは以下のようになっています。
code:project.clj
そして話をするための簡易的なプロジェクトのベースはこんな感じです。ちょっと長いし本質とはあんまり関係ない(ないこともないけど)ので、とりあえず忘れてもらってて大丈夫です。
code:clojure
(require
(defonce +system+ (atom nil))
(defmethod ig/init-key :demo/server
opts
(jetty/run-jetty (or (:handler opts) identity))))
(defmethod ig/halt-key! :demo/server
server
(.stop server))
(def routes
["/" {:get :handler/get-handler
:post :handler/post-handler}])
(defmulti handler-resolver identity)
(defmethod ig/init-key :demo/endpoint
_
(-> (bidi.ring/make-handler routes handler-resolver)
mw.keyword-params/wrap-keyword-params
mw.nested-params/wrap-nested-params
mw.params/wrap-params))
(def system-config
{:demo/endpoint {}
:demo/server {:port 3000
:join? false
:handler (ig/ref :demo/endpoint)}})
(defn start []
(when-not @+system+
(reset! +system+ (ig/init system-config)))
:started)
(defn stop []
(when @+system+ (ig/halt! @+system+))
(reset! +system+ nil)
:stoped)
本題は以下のコードからです。
code:clojure
(def view
(h/html
[:form {:method :post}
[:div
[:div
[:input {:name "aliceage"}]] [:div
[:div
[:input {:name "bobage"}]] (-> view
res/response
(res/content-type "text/html; charset=utf8")))
(-> (:params req)
pr-str
res/response
(res/content-type "text/plain; charset=utf8")))
(defmethod handler-resolver :handler/get-handler
get-handler)
(defmethod handler-resolver :handler/post-handler
post-handler)
handler-resolverのところはとりあえず無視してもらってもいいんですが、注目してほしいのはviewの各inputタグにあるname属性です。Railsなんかをやっている方だと馴染みの深い記法なのではないでしょうか。また、他の言語の他のフレームワークなどでも、ネストしたものを扱いたいときは似たような記法が用意されていることが多いと思います。 そして、これをサブミットすると次の結果を得ることができます。
code:clojure
{:alice {:hometown "Saitama", :age "19"}, :bob {:hometown "Tokyo", :age "20"}}
良い感じの結果を得ることができます。ところで一体なにが問題なのでしょうか?まずはHiccupについておさらいしてみましょう。 code:clojure
;; => "<input name=\"alice.hometown\" />"
Hiccupは通常name属性をキーワードで記述することができます。ですが、先程のようにネストしたデータを扱いたい場合は文字列で記述する必要があります。別にいいじゃないか、という声が聞こえてきそうですね。別にいいんですが、Clojureにはこのような構造を扱うためのもっと良い方法があったような気がしませんか?そう、それが今回話題にあげたい名前空間付きキーワードです。 早速Hiccupで名前空間付きキーワードを使ってみましょう。 code:clojure
;; => "<input name=\"hometown\" />"
…?hometownだけしか残りませんでした。これはHiccupがname関数を何も考えずに利用しているからですね。ちなみに文字列で同じようにするとどうなるかというと、次のようになります。 code:clojure
;; => "<input name=\"alice/hometown\" />"
期待通りの結果を得ることができましたね。ただ、キーワードとして記述したいので、ちょっとしたトリックを使いましょう。HiccupのデータをHiccupのデータに変換してあげる前処理を入れてみます。 code:clojure
(defmethod k.growing/transform-by-tag :input
_ tagvec
`[~tag
(update ~tagopts :name #(if (keyword? %) (subs (str %) 1) %)) ~@contents]))
このようなメソッドを定義すると以下のように記述することができます。
code:clojure
;; => "<input name=\"alice/hometown\" />"
これで名前空間付きキーワードで書いても、Hiccupは期待通りに変換してくれます。これを利用してviewを書き換えてみましょう。
code:clojure
(def view
(h/html
(k.ultimate/transform*
[:form {:method :post}
[:div
[:div
[:div
[:div
この状態で画面を表示して、サブミットすると以下のような結果を得ることができます。
code:clojure
{"alice/hometown" "Saitama", "alice/age" "19", "bob/hometown" "Tokyo", "bob/age" "20"}
code:clojure
;; 良い子はこれをこのまま真似しないで、上のPRにあるようなコードと等価になるようにするなどしてください
ここまでするとようやく求めた結果を得ることができるようになります。
code:clojure
{:alice/hometown "Saitama", :alice/age "19", :bob/hometown "Tokyo", :bob/age "20"}
さて、名前空間付きキーワードにするメリットは実際どのくらいあるんでしょうか?パッと浮ぶ利点は以下の通り。
分配束縛がスマートになる
任意の値へのアクセスが容易になる
補完が効くので間違い難い
code:clojure
(def nested
{:alice {:hometown "Saitama" :age "19"}
:bob {:hometown "Tokyo" :age "20"}})
(str hometown ", " age))
;; => "Saitama, 19"
;; => "Saitama"
(def flat
{:alice/hometown "Saitama" :alice/age "19"
:bob/hometown "Tokyo" :bob/age "20"})
(str hometown ", " age))
;; => "Saitama, 19"
(:alice/hometown flat)
;; => "Saitama"
ほとんどの人には共感してもらえないかもしれないんですが、特に「任意の値へのアクセスが容易になる(get-inを利用せずキーワードを関数として扱える)」というのはマクロなどを書く際にとても重要で、拙作のkuugaを利用して「フォーム上にある全てのinputタグのvalue属性を前回の入力値で埋める」みたいなことを実現するときには重宝しました。古い昔ながらのやり方に従い続けるのではなく、その言語らしいやり方みたいなのを模索するのも悪くないですよね。 ここまでに出てきたコードの完全版は以下にあります。
ちなみにこれをやるとnested-paramsが不要になったように見えますが、実際はそんなことなくてusers[0]みたいに数値の添字を期待するようなパターンには対応できないです。まあ、そういうのが必要な場合は諦めましょうってことで笑
余談
途中で出てきたkuugaですが、同僚のuochan.iconに「よくできている」とお褒めの言葉をいただけたので最高です。わりとオーソドックスな昔ながらのWebアプリケーションを作るときには重宝すると思うので、是非使ってみてください(面倒なHiccupデータの繰り返しを省略できたりします)。汎用性が高いので色々な用途で利用できます :) 宣伝
ところでCybozu Startups, inc.では、Clojureを書きたいエンジニア募集してます。もし、Clojureを書きたくて興味があって、話だけでも聞いてみたいという方はayato-p.iconまでご連絡ください(/icons/twitter.iconのDMあたりで)。カジュアルにお寿司を食べながらお話しましょう :)