Clojure Web Development勉強 - Ring (その5) ルーティング
というわけでついにルーティング。
そもそもルーティングとは?
今まではどのリンクを踏んでも同じコンテンツしか見えないという「お前htmlメモ帳に直打ちしてftpでアップロードしてた頃のサイトの方がよっぽど動的なんだけどどうなの?」と煽られてしまうような有様だったのだが、ようやく「uriやparamsによってどのようなページを表示するかをプログラム上で決定する」という機能を実装する。この機能がルーティング。
前回のプログラムにルーティングを実装したコードがこちら:
変更したのはhandler.cljとcore.cljの2ファイル。
handler.cljでの変更: - get/postからのクエリパラメータを使って非常に簡単な動的HTML作成を実施するdisplay-result関数 - リクエストマップの中のuriやparamsフィールドなどを調べ、正しいhandlerへのルーティングを担当するhandler-with-routing関数 - コードの整理(使わないplain-textやredirect関数を削ったり、微妙に表示されるhtmlを変更したり)
core.cljでは単にhandler-with-routing関数を大本のhandlerとして使うように変更したのみ。
display-result関数の中身:
(defn display-result [req] (let [{:keys [params uri]} req param-name (get params "name") req-type (if (= uri "/get-submit") "GET" "POST")] (-> (res/response (str "<div> <h1>Hello " param-name "!</h1> <p>Submitted via a " req-type " request.</p> <p><a href='..'>Return to main page</p> </div>")) (res/content-type "text/html"))))
destructuringでリクエストマップからparamsとuriをとり、さらにparamsから"name"フィールド、uriがpost-submitかget-submitかによってreq-typeを"GET"か"POST"に束縛して、名前とリクエストタイプを表示する。言ってしまえばそれだけだが、最低限の動的な機能は備えている。
そしてルーター部分:
(defn handler-with-routing [req] (println "In Routing Handler!") (let [{:keys [uri params request-method]} req] (cond (= uri "/") (main req) (= uri "/get-form.html") (get-form req) (= uri "/post-form.html") (post-form req) (= uri "/get-submit") (display-result req) (= uri "/post-submit") (display-result req) :else (not-found req))))
ちなみに私はどーしてもラウター、ラウティングと読んでしまう・・・
まあボイラープレートっぽい(= uri ...) (... req)
が並んでいる時点でLisperなら誰でも「おっ、マクロの時間だ」と思うわけで、近いうちに使ってみることになるCompojureというライブラリも基本的にこの部分を簡略化した上で強力なパターンマッチング機能を提供するマクロ群だ。
しかし、見栄えは別にして、これでこのサーバ上で何かのページにアクセスしようとした場合はこの関数を通してuriマッチングが行われて正しいページに送られるようになったわけだ。この関数に載っていないuriの場合は:elseのところにかかって(not-found req)から404メッセージが表示される。
ここからはring以外のライブラリ、特にcompojureとhiccupを見ていきたい。compojureでルーティング周りを、そしてhiccupでhtmlページの記述をより洗練させていきたい。
Clojure Web Development勉強 - Ring (その4) モジュール化とGET/POST Form
次回はルーティングだと言ったな、あれは嘘だ。
その前に、まず今まで書いたコードを
- handler.clj (responseを実際に返す関数群)
- middleware.clj (handlerをラップしてログを取ったりする機能を実装する関数群)
- core.clj (それ以外。main関数っぽいのも含めて)
に分割してみた。
そしてGET/POST formを使ってクライアントからサーバ側により自由な形式で情報を送れるようにしてみた。
そのコードがこれ:
分割したので、何かのファイルを更新するたびに(require simple-modular-ring.core :reload-all)
としてREPLに全部のdependencyをリロードさせる必要が生じている。vim fireplaceだとcore.cljのファイルから:Require!
でやってくれるので便利。
コードの説明をすると、まずmiddlewareで
(ns simple-modular-ring.middleware (require [ring.middleware.params :as mparams])) (def wrap-params mparams/wrap-params)
ringが提供する便利middlewareの一つであるwrap-paramsを使っている。paramsとはGETやPOSTメソッドでクライアントから渡されるurl以外の情報(例えばフォーム入力)を集めたもの。wrap-paramsを使わないと、特にPOSTメソッドの場合その情報がリクエストマップから失われてしまう。ここら辺の事情についてはringの公式ドキュメントが詳しい。
そして今回は結構リクエストマップを調べるので、リクエスト・レスポンスを@log-atomに入れるだけじゃなくてreplに直接pprintしてくれるmiddlewareを作った。
(defn req-res-displayer [handler] (fn [req] (let [res (handler req)] (println "\nRequest:") (clojure.pprint/pprint req) (println "\nResponse:") (clojure.pprint/pprint res) res)))
pprintした後、ちゃんとレスポンスを返り値として書いておくのを忘れないように・・・
さて、次は今回の主役と言っていいGETとPOSTのフォームを表示するハンドラである。
まずはGET:
(defn get-form [req] (-> (res/response "<div> <h1>Hello GET Form!</h1> <form method=\"get\" action=\"form-submit\"> <input type=\"text\" name=\"name\" /> <input type=\"submit\" value\"submit\" /> </form> </div>") (res/content-type "text/html")))
これをappのハンドラとして設定すると、リクエストマップが大体以下のようになる:
{;いろいろ略 :protocol "HTTP/1.1", :params {}, :headers {;以下略 }, :server-port 8080, :form-params {}, :query-params {}, :uri "/", :server-name "localhost", :query-string nil, :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x89db6db "HttpInputOverHTTP@89db6db"], :scheme :http, :request-method :get}
で、フォームに適当な文字列を入れてSubmitを押すと、アドレスバーのURLが"localhost:8080/form-submit?name=Test%21"となる。REPLを調べてみると新しくこのようなリクエストマップが投げられているのがわかる:
{;さらにいろいろ略 :params {"name" "Test!"}, :form-params {}, :query-params {"name" "Test!"}, :uri "/form-submit", :query-string "name=Test%21", :request-method :get}
params、query-params、query-string全部に"Test!"という文字列が含まれている(query-stringの場合アドレスバーに!が表示できないので%21になっているが)。この情報を受け取って、ハンドラはよりカスタムなページを作成・表示させることができる。例えばその文字列をHelloの後に表示するとか。
POSTも大体流儀は同じなのだが、そもそもアドレスバー経由じゃないのでquery-paramsにはならない。
(defn post-form [req] (-> (res/response "<div> <h1>Hello POST Form!</h1> <form method=\"post\" action=\"form-submit\"> <input type=\"text\" name=\"name\" /> <input type=\"submit\" value\"submit\" /> </form> </div>") (res/content-type "text/html")))
こっちのフォームに文字列を入れるとURLが"localhost:8080/form-submit"に変わり(今度はqueryの内容はURLに反映されない)、サーバの方ではこんなリクエストを投げてくる:
{;またしても省略 :params {"name" "Test!"}, :form-params {"name" "Test!"}, :query-params {}, :uri "/form-submit", :query-string nil, :request-method :post}
やはりクエリには何も含まれておらず、paramsとform-paramsに情報が入ってくる。前述の通り、これらはwrap-paramsを使わないとリクエストマップに含まれない。
GETとPOSTで大体同じ機能を実装してみたわけだが、HTMLやウェブ入門的な文書によると、基本的にリクエストに応じて情報を引っ張ってくるだけ&クエリ内容にプライバシーの心配がない場合GETを使い、副作用的な効果のあるリクエスト(掲示板への投稿など)や情報を秘匿した方がいいもの(ユーザ名やパスワード)はPOSTを使う、らしい。POSTを使うのも、今のベストプラクティスかどうかはわからないのでその部分は信用しないでいただきたい。
そして次回は今度こそ、uriやparamsを用いて動的にどのページを表示するか、ringアプリの方で決めるコード、つまりルーティング機能を(とりあえずまずはringだけで)実装していく。
Clojure Web Development勉強 - Ring (その3) ResponseとHandler
いろんなresponseの作成を助ける関数群のring.util.responseを少し使ってみる。
前回handlerと読んでいた関数をplain-textと改名し、それに加えてhtmlを返すhtml関数、404を返すnot-found関数、そしてhttp://www.google.comにリダイレクトするredirect関数を定義した。
html関数:
(defn html [req] (-> (res/response "<div> <h1>Hello World!</h1> <p>Some random bit of text</p> </div>") (res/content-type "text/html")))
content-typeがtext/htmlになっているのと、bodyに含まれる文字列が実際のhtmlタグを使った文書になっている。とりあえず、現在は最低限のring機能の紹介ということでテンプレートを使っていない。実際のサイト作成ではまずこのようなhtmlタグのベタ書きはせず、テンプレートライブラリを使うことになる。私はfront-endでreagentを使っているので、それに合わせてhtmlテンプレートはhiccupというclojureデータ構造をフル活用するライブラリを使おうと考えている。
not-found関数:
(defn not-found [req] (-> (res/not-found "<b>Not found!</b>") (res/content-type "text/html")))
res/not-found
を使うことによってレスポンスマップは:status 404
になる。やはりこれも現在はタグベタ打ち。
redirect関数:
(defn redirect [req] (res/redirect "http://www.google.com"))
これで問答無用でグーグル先生に飛ぶ。
あとはこれらのハンドラ関数のどれかを
(def my-app (-> plain-text ;swap this with another handler function logger))
のplain-text
のところに代入してreplに送ればlocalhost:8080
にブラウザでアクセスした時の挙動が変わる。
次回はルーティング、つまりuriに指定されたパスによって表示するコンテンツを変えるという機能を付け加えてみる。
Clojure Web Development勉強 - Ring (その2) コード詳解
とりあえず前回張ったコードの説明をしておく。
「詳解も何もそのまんまだろうが(怒」と言われたら「その通りですハハーッ(平伏」と言うしかないのだが、まあ自分のアウトプット&備忘録を兼ねて。
まずproject.clj。超簡単。というかlein newでできたものを削りまくった後で、ちょこっと1行足して作っている。
(defproject ring-min-log "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.8.0"] [ring "1.5.0"]])
足したのは[ring "1.5.0"]
の部分だけ。
ringは4つのライブラリに分割されていて、必要なものだけdependenciesに記述する方式でもいける。というか多分そちらの方が推奨かと思う(特に本番に向けた開発では)。今回はできるだけ単純なままに留めたい、ということでring全てを一括でdependencyとして導入している。
ちなみに分割されたライブラリは以下のとおり:
- ring-core - essential functions for handling parameters, cookies and more
- ring-devel - functions for developing and debugging Ring applications
- ring-servlet - construct Java servlets from Ring handlers
- ring-jetty-adapter - a Ring adapter that uses the Jetty webserver
(ソース)
今回はring-coreとring-jetty-adapterがあれば充分。(他の二つはイマイチ何ができるのかわかっていない)
次はcore.clj。これはsrc/ring_min_log/に入っている前提。当然ながら冒頭はnamespaceの定義とnamespace内で使うライブラリの宣言。
(ns ring-min-log.core (require [ring.util.response :as res] [ring.adapter.jetty :refer [run-jetty]] ))
これでring-coreからring.util.responseを、ring-jetty-adapterからring.adapter.jettyをとってきている。これらの説明は、実際に使っているコードのところで行う。
ということでこれ:
(defn handler [req] (-> (res/response "Hello Goodbye") (res/content-type "text/plain")))
ハンドラとはリクエストマップを受け取りレスポンスマップを返す関数であり、ringでwebアプリを作る場合、このハンドラがほとんどの動的ページ作成ロジックを担う。
上記のコードはなんらかのリクエストマップに対して、"Hello Goodbye"という文字列を表示させるレスポンスマップを返すだけのハンドラを作っている。
前述の通り、ringの最大の責務はHTTP requestをclojureのimmutable mapに変換してくれ、そしてまた開発者が書いたコードの最終的な戻り値であるimmutable mapをHTTP responseに変換して出力してくれることだ。
レスポンスに変換されるimmutable mapは、もちろん自分で(必要なフィールドを全て指定して)書くこともできるのだが、デフォルト値などもちゃんと設定してくれてレスポンスマップ作成を相当簡易にしてくれる関数がいろいろとring.util.responseに入っている。
(res/response "Hello Goodbye")
で{:status 200 :header {} :body "Hello Goodbye"}
というマップを作成してくれ、そのマップを(res/content-type % "text/plain")
に入れることでそこに:header {"Content-Type" "text/plain"}
が加わる。
直に書くなら
(defn handler [req] {:status 200 :header {"Content-Type" "text/plain"} :body "Hello Goodbye"}
になる。
次に、リクエストマップとレスポンスマップを全部保存してreplから確認できるようにしたい!ということで、その機能をmiddlewareとして実装してみる。
middlewareとは、ハンドラ関数を引数に取り、なんらかの機能を付け加えた新しいハンドラ関数を返す高階関数である。
(defonce log-atom (atom [])) (defn logger [handler] (fn [req] (let [res (handler req)] (swap! log-atom #(conj % {:request req :response res})) res)))
loggerが返す新しいハンドラは、
- まずリクエストマップを受け取り
- loggerの引数となった元のハンドラに渡してレスポンスマップを作成
- 副作用としてlog-atomにそのリクエストマップとレスポンスマップを追加
- 最後にレスポンスマップを返す
という関数になっている。これでreplからlog-atomの中身を調べて、どういうリクエストに対してどういうレスポンスが返されているのか、非常に簡単に参照することができる。バリバリ副作用なのが悲しいところだが・・・
次はhandlerとloggerの組み合わせ:
(def my-app (-> handler logger))
ここはhandlerをloggerに渡して新しいmy-appハンドラを作成しているだけ。middleware一つだけなのでthreading macroのありがたみが薄いが、これでいろんなmiddlewareを重ねることで様々な機能を追加していくことが簡単になる。
最後にサーバの立ち上げとハンドラの登録:
(defonce server (run-jetty #'my-app {:port 8080 :join? false}))
Javaで書かれたJettyというHTTPサーバ兼Java Servletコンテナを使い、replが使えるようにnon-blockingな設定でポート8080からのリクエストをマップに変換したのちにmy-appに投げ、my-appからの戻り値をHTTPレスポンスに変換したのちにクライアントに返すサーバを立ち上げている。
defonce
と#'my-app
としているのはやはりrepl開発のため。
defonceのおかげでコードを全部再評価させてもこの部分は読み込まれない(サーバを新しく立ち上げない)。これがないと、とりあえず再読み込み時にサーバを閉じるロジックが必要になる。
しかし、素直に(defonce server (run-jetty my-app {:port 8080 :join? false}))
と書くとmy-appの定義がdefonceされた時点でのものに固定されてしまい、コードに対する変更が全く反映されなくなってしまう。それを避けるためにclojureのvar-quote dispatcherマクロ#'
を使い、run-jettyにmy-appそのものではなくmy-appへの参照を渡すことで、my-appが更新されたらserverも最新の定義を使うようにしている。
あとはこのcore.cljをREPLで評価し、ブラウザでlocalhost:8080に繋げればオッケー。例えばREPLから
user=> (ns ring-min-log.core) ring-min-log.core=> @log-atom
でリクエストマップとレスポンスマップが見れたりする。
Clojure Web Development勉強 - Ring (その1)
Clojureで囲碁対局サイトを作ってみたい - Arantium Maestum
でも書いた通り、中規模ウェブアプリ的なものを開発してみたい。そのためのライブラリについて、色々と覚書をメモっておく。
Clojureでウェブ開発する場合、サーバサイドの根幹はほぼ必ずringというライブラリを使うようだ。
HTTPサーバとのリクエスト・レスポンスのやり取りを担当するライブラリである。
サーバからのリクエストをClojureお馴染みのimmutableなmapデータに変換し、そしてサーバ側に投げ返すレスポンスもまたimmutableなmapから自動的に作成してくれる。そうすると、開発者側ではリクエスト情報が含まれるデータ構造を受け取り、レスポンス情報が含まれるデータ構造を返す純粋関数を作成することに注力するのみとなり、関数型プログラミング好きとしてはなかなか嬉しいことになる。あと、各種サーバのAPIの違いなどをringが吸収してくれるというメリットも大きい(ようだ)。
いつもながらathosさんのご教示に感謝。
@zehnpaard RingというのはHTTPリクエストをマップでもらってレスポンスをマップで返せばOKという仕組みを作っている部分で、Compojureは基本的にはどのエンドポイントへのリクエストでどの処理を実行するかを決定するルーティングを担います。
— えいとす)))))))) (@athos0220) 2016年10月25日
大抵はcompojureやbidiといったルーティングライブラリと合わせて使うことがほとんどらしい。compojureあたりは結構ringの実装を隠蔽してより高次なレベルで書けるようにしてくれる部分もあるみたい。今はとりあえずring自体と慣れ親しみたいのでcompojureは使わないでおく。
まあかなり最低限なプロジェクトとして、どんなリクエストにも同じ文字列を返すハンドラと、そのハンドラを包む形でリクエストとレスポンスをatomにログする高階関数、サーバとしてJetty、そしてそれらをreplで走らせられるような簡単な枠組みを作ってみた。
ちなみにこのloggerがいわゆるmiddlewareというパターンで、高階関数としてハンドラに機能を追加していく。ringライブラリ自体にも便利なmiddlewareがいろいろあるみたいなので調べていきたい。
ともかく、このコードをreplで走らせればサーバがreplをブロックすることなく立ち上がるので、ブラウザでlocalhost:8080/開けば確認できる。
コード変更があったらとりあえず全部Evalしてウェブページをリロードすれば反映されるし、replで@log-atomを参照することで時系列順にリクエストとそれに対するレスポンスがベクタに入っているのを直接見ることができる。というか、ページロードごとにリクエストが二つ(ページとfavicon.ico分)送られてるの、初めて知った・・・
これでとりあえずいろいろいじりながらrequest/responseともに触って確認できる環境が整った。middlewareを含むringの機能をさらに使い倒してみようと思う。
追記:当然ながら、gistに乗っけたコードは自分の開発・勉強のために役に立つlogger機能であって、本番環境で使うとメモリリークするのでやめましょう、ととりあえず無意味な保身をしておく。
Clojureで囲碁対局サイトを作ってみたい
Clojureで中規模なWebアプリ開発を個人プロジェクトとしてやってみたい。中規模のイメージとしてはTODOアプリ以上、eコマースサイト(これって死語?)未満。
というわけで、囲碁対局サイトを作ってみようと思う。
データベースは当然として、ユーザ認証、ウェブソケット、Clojurescriptを使った動的なフロントエンドあたりを盛り込んでいく。
LuminusやArachneのような、複数のライブラリを統合するようなフレームワークは極力使わず、ある程度低レベルなライブラリをいじりながら作る。
とりあえず使うのがほぼ決定なライブラリとしてはring, compojure, hiccup, garden, reagentあたり。re-frameはどうしよう。web socketはsenteを使うのもアリか。デプロイはとりあえずHerokuかなぁ。
作りたいものを思いつくままにとりあえず書き出してみると:
- 現在進行中の対局リスト
- 現在参加可能な対局リスト
- ログイン中のユーザリスト
- ユーザに対する対局申し込みと承認・拒否
- 対局ボードの対局・閲覧モード
- 終局後の判定システム
- 対局中・感想戦用のコメントシステム
- 過去の棋譜のログ閲覧とエキスポート機能
- 各種AI(まあ初めはGNU GoとFuegoあたり)の追加
- 自分の対局成績の閲覧と分析
- モバイル対応のレスポンシブデザイン(スマホだとタップ一回はズームとか?)
あとは自分でAI実装してみたり、詰碁やらの問題集機能をつけてみたり、と色々やれることは多そう。
まあボード実装してオープンソースAIにつなげればとりあえずは自分で使えるようになるから、そうやってモチベーションを維持してぼちぼちと進めていこう。進めていければいいな。
Clojureでゴールドベルク、なビデオ
不覚にもちょっと感動してしまった。と同時に「やりたいことやられちゃった」感もあり。そうだよね、Overtoneやってたらとりあえず対位法が一つの到達点だよね。
githubにちゃんとコードが載ってる。
まずはこのコードをきっちり読み込んで自分のものとしたい。一ついじってみたいところとしては、Processing/Quilのようにpush-matrix/pop-matrix的にコンテキストを局所化しつつコントロールすることで、単体として独立したパターンのものをさらに組み合わせていけるか試してみたい。
つまり、より高い抽象性に移行せずに、再帰的な形で音楽のフラグメントを組み合わせていけるか。
それがプログラミング作法的に正しいやり方かというと疑問が残るが、絵や音楽の作り方としてはより直感的なように思う。