Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - Ring (その2) コード詳解

とりあえず前回張ったコードの説明をしておく。

gist.github.com

「詳解も何もそのまんまだろうが(怒」と言われたら「その通りですハハーッ(平伏」と言うしかないのだが、まあ自分のアウトプット&備忘録を兼ねて。

まず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が返す新しいハンドラは、

  1. まずリクエストマップを受け取り
  2. loggerの引数となった元のハンドラに渡してレスポンスマップを作成
  3. 副作用としてlog-atomにそのリクエストマップとレスポンスマップを追加
  4. 最後にレスポンスマップを返す

という関数になっている。これで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というライブラリを使うようだ。

github.com

HTTPサーバとのリクエスト・レスポンスのやり取りを担当するライブラリである。

サーバからのリクエストをClojureお馴染みのimmutableなmapデータに変換し、そしてサーバ側に投げ返すレスポンスもまたimmutableなmapから自動的に作成してくれる。そうすると、開発者側ではリクエスト情報が含まれるデータ構造を受け取り、レスポンス情報が含まれるデータ構造を返す純粋関数を作成することに注力するのみとなり、関数型プログラミング好きとしてはなかなか嬉しいことになる。あと、各種サーバのAPIの違いなどをringが吸収してくれるというメリットも大きい(ようだ)。

いつもながらathosさんのご教示に感謝。

大抵はcompojureやbidiといったルーティングライブラリと合わせて使うことがほとんどらしい。compojureあたりは結構ringの実装を隠蔽してより高次なレベルで書けるようにしてくれる部分もあるみたい。今はとりあえずring自体と慣れ親しみたいのでcompojureは使わないでおく。

まあかなり最低限なプロジェクトとして、どんなリクエストにも同じ文字列を返すハンドラと、そのハンドラを包む形でリクエストとレスポンスをatomにログする高階関数、サーバとしてJetty、そしてそれらをreplで走らせられるような簡単な枠組みを作ってみた。

gist.github.com

ちなみにこの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でゴールドベルク、なビデオ

www.youtube.com

不覚にもちょっと感動してしまった。と同時に「やりたいことやられちゃった」感もあり。そうだよね、Overtoneやってたらとりあえず対位法が一つの到達点だよね。

githubにちゃんとコードが載ってる。

github.com

まずはこのコードをきっちり読み込んで自分のものとしたい。一ついじってみたいところとしては、Processing/Quilのようにpush-matrix/pop-matrix的にコンテキストを局所化しつつコントロールすることで、単体として独立したパターンのものをさらに組み合わせていけるか試してみたい。

つまり、より高い抽象性に移行せずに、再帰的な形で音楽のフラグメントを組み合わせていけるか。

それがプログラミング作法的に正しいやり方かというと疑問が残るが、絵や音楽の作り方としてはより直感的なように思う。

Clojure/Overtoneで遊んでみる

SICPばかりでもなんなので、Clojureで音楽が作れるっぽいOvertoneライブラリと戯れてみる。

Overtoneプロジェクト:

github.com

参考にさせていただいたブログ:

Overtone: Clojureで音楽を書こう : サルノオボエガキ

とりあえずdependenciesを足して、イントロビデオを見て、lein replでコードをコピペして弄ってみる。

github.com

(use 'overtone.live)(use 'overtone.inst.piano)で当面は十分そう。

ピアノ音を鳴らす関数がそのまんまのpiano。

(piano)あるいは(piano 50)などのように使う。

(piano)で出る音が(piano 60)と同一で、mid2Cのようだ。そこから数が上がる・下がるにつれ半音ずつずれていく。

「ドミソ」のコードは以下のようになる。

(doseq [note [60 64 67]]
  (piano note))

時間を指定して鳴らすには(at XXX (piano 60))といった構文を使う。

以下のコードで「ドレミ」と1秒ずつずれて鳴る。

(let [time (now)]
  (doseq [i (range 3)]
    (at (+ time (* i 1000)) (piano (+ 60 i)))))

基本的には通常のClojureの構文であらかたコントロールできてしまう印象。

とりあえず、音のリストを受け取り、順番に鳴らしていく関数。全ての音符が同じ長さで鳴る(というか等間隔で鳴る)。

(defn play-me [notes]
  (let [time (now)]
    (doseq [[i note] (map vector (range) notes)]
      (at (+ (* 1000 i) time) (piano note)))))

あとは色々試すだけ。

(play-me [60 55 60 65 64 60 62 60 60])

とか

(play-me [60 55 60 65 64 60 62 64 64])

とか。

移調するのも(map #(- % 2) notes)などで出来てしまうのが楽しい。

やはり音符の長さは指定したい。ということでplay-meを微調整:

(defn play-me
  ([notes] (play-me notes (range)))
  ([notes length]
    (let [time (now)]
      (doseq [[i note] (map vector length notes)]
        (at (+ time (* i 1000)) (piano note))))))

(def notes [60 55 60 65 64 60 62 60 60])
(def length [1 1 1 1 1 1 1.5 0.5 1])
(def cum-length (reductions + 0 length))
(play-me notes cum-length)

あとは休符をどうするかだなー。もっと遊びながら考えていきたい。

SICPの勉強 問題2.30~32

2.30

木構造のデータ(nestしたリスト)の全ての葉ノードを二乗する関数:

(defn square-tree [tree]
  (cond
    (empty? tree)
    ()
    
    (seq? (first tree))
    (cons (square-tree (first tree))
          (square-tree (rest tree)))
    
    :else
    (cons (sq (first tree))
          (square-tree (rest tree)))))

2.31

木構造のデータ(nestしたリスト)の全ての葉ノードに任意の関数を適用する関数:

(defn tree-map [f tree]
  (cond 
    (empty? tree)
    ()
    
    (seq? (first tree))
    (cons (tree-map f (first tree))
          (tree-map f (rest tree)))
    
    :else
    (cons (f (first tree))
          (tree-map f (rest tree)))))

普通のmapを使って書き直すなら:

(defn tree-map-m [f tree]
  (if (not (seq? tree))
    (f tree)
    (map #(tree-map-m f %) tree)))

例によってヴェクタは存在しないものとする。まあ、リストと数字しか存在しないという過程はSICP的にはそんなに荒唐無稽な仮定ではないと思う。

2.32

リストの要素から作り得る全ての部分集合を返す関数:

(defn subsets [s]
  (if (empty? s)
    ()
    (let [current-element (first s)
          subsets-of-rest (subsets (rest s))]
      (concat subsets-of-rest 
              (map (fn [subset] 
                     (cons current-element subset)) 
                   subsets-of-rest)))))

SICPの勉強 問題2.27~28

2.27

リストの要素を逆順にするだけでなく、そのリストに含まれる全てのリストも同時に逆にする関数:

(defn deep-reverse [x]
  (if (not (seq? x))
    x
    (loop [items  x
           result ()]
      (if (empty? items)
        result
        (recur (rest items)
               (cons (deep-reverse (first items))
                     result))))))

Dynamic typing万歳、と言いたくなるようなゆる〜い実装。あと、seq?だとリストやconsでできた遅延リストは大丈夫だがvectorはダメ。

2.28

nested-listを完全にflatten化する関数:

(defn fringe-r [tree result]
  (cond
    (empty? tree)
    result
    
    (seq? (first tree))
    (fringe-r (rest tree)
              (fringe-r (first tree) result))
    
    :else
    (fringe-r (rest tree)
              (cons (first tree) result))))

(defn fringe [tree]
  (reverse (fringe-r tree ())))

とりあえず再帰とconsだと逆さまに結果が出てくるので、一旦別関数で逆順の結果を出させてreverseで直している。