Clojure-n
すいません遅れました。この記事は Aizu Advent Calendar 2017 向けに書かれました。
前日は id:mic_psm さんの 部屋を探す話 でした。ヅドベント参加者のみなさんお疲れ様でした。
はじめに:挨拶
ヅドベント 25 日目を担当します cl_yaho です。大学院では延々と Matlab 書いてました。
今回書く内容には「はやりのやつ」と書きましたが、はやりといえば Clojure です。心無しかクリスマスと語感が似ている気がします。メリークリスマス!
対象読者
Clojure に興味がある人、最近始めた人など。 この一ヶ月 Clojure を書いていたので、書く時に考えていたことや参考にした記事などをまとめた。
前置き:内容と Lisp について
このアドベントカレンダーに登録した頃に日本で Clojure に関するスピーチ が話題になっていたのではやりということにした。以前行われていた日本の Clojure に関するイベント Clojure/conj 2017 上映会#1 では スピーチを日本語でまとめたスライド が公開されており、これが大きなきっかけとなったようだ。プログラミング言語である Clojure を作成している人間の思想を少し覗くことができる。
私は Lisp が好きであり、あえて特定のものではなく「Lisp」 が好きというのは、今存在している様々な Lisp と、そしてその Lisp と様々なプログラミング言語を糧にして今後産まれてくるであろう新しい Lisp に対する思いからである。 Lisp の良さの一つはそういった生命体的な存在であることが挙げられると思う。今回はその中でも Clojure について書いていく。
Clojure は丁度 2 年ほど前に機械学習のアルゴリズムを実装してみるなどして少し触っていたのだが、その後の 2 年は研究でもバイトでも Matlab を使っていた。なので久しぶりの再開ということになった。
Clojure を知らない人の興味をそそるかもしれないおもしろい情報としては、Typed Clojure という、漸近的型付けと呼ばれる技術を使った型システムライブラリがあったり *1 、または Spec という、述語をベースにした検証システムライブラリなどもある。更に ClojureScript という Clojure とほぼ同様に書ける AltJS が存在したりする。 *2 こういった機能がライブラリとして使える (言語基盤として実装されていない) ことが可能であることも Clojure を含む Lisp のおもしろいところだ思う。
ただし、この一ヶ月 Clojure を書いたり読んだり、それ関連の記事を漁ったりして感じたことは、 Clojure では「複雑な機能は必要になった時に使う」というような思想があるようであるということだ。例えば始めから型システムの恩恵を受けながらプログラミングがしたいと思っているのであればそういう言語を使うべきで、型システムや検証システムを持つ言語として扱われることを Clojure は想定していないように思う。まずはマップを中心としたシーケンス処理で実現する。それで目的が達成可能であればそれでよく、難しい場合に Clojure の上で実現されている様々な機能 (ライブラリ) を利用していく、という姿勢が正しいように思った。間違っているかもしれない。
前置きが長くなったが今後はひとまず Clojure を使ってやっていきたいと言うことと、就職先で Clojure/ClojureScript を使うことになったということ、さらにこれまであまり手を動かしてこなかったことに対する反省から「Clojure で何か n 個くらい作ろう」と思い立ち Clojure-n
プロジェクトを先月下旬に発足した。今回の記事では、先月末からそのプロジェクトの作品ということで作ったものを、作っている時に参照した場所や考えていたことなどを踏まえてまとめたい。
Clojure-n
Clojure-n は n 個のテーマのもと何か物を作りましょうというもので、 n は任意の整数である。
やる気に満ちていたので今日までに 100 個くらいできている予定だった。しかし力が及ばず公開できるのは 2 つのテーマになってしまった。
Web
初めのテーマはきっかけがあり Web になった。 Clojure どころか Web 上で動くものを作ることが人生でほぼ初めてだったが、今回作ったものは大変な自信作になったので Heroku を利用し公開した。
ルーティングも実装済みである。
揺れる oxtu に HTML タグを付けて遊べます。
参考にしたと言ったら怒られそうだが Clojure で Web 開発をはじめてみよう を参考にした。初めて Web をやる人間が躓くであろう概念について説明してくれているのでとてもありがたかった。上のページのコードは Github - iyahoo/oxtu に一応上げたが現在は実験場になってしまっているので Github - iyahoo/oxtu/tree/c07125e25b449432957dc968c これがコミットがほぼ最小の状態になっている。
また上記の入門記事を書いている方がその続きとなる記事を書いている。 ClojureでWebアプリケーション開発がしたい初心者の方へ 私はそもそも Web における「フレームワーク」というものに全くピンと来ないので、今後作っていくものの中で実際使いながら確かめたい。
レイトレーシング
数年前からレイトレーシングしたいという思いがあったので本を読んで勉強した。23 日に担当していた @yopio_ 君のオススメで 週末レイトレーシング を読んだ。この本では実装言語に C++ を使用しているので、そのコードを Clojure で書き写す形で勉強したのだが、折角 Clojure で書くので以下のことを意識して書き直した。
- 小さい関数を組み合わせるように書く
- 可能な限りシーケンスに対する処理の連鎖になるようにする
- 副作用は必要でなければ基本使わない
このあたりの Clojure を使う上での考え方などについては以下を参考にした。
特に最初の書籍は参考になった。現在 Clojure は version 1.9.0 が出て、この書籍は version 1.3.0 の頃のものなのだが、最近になっていろいろと記事などを漁ってみても大きな間違いなどを指摘していたりするものは特に見つからなかったので内容は全く古くなっていないようだ。現在も入門書として読んで問題ないと思う。また Clojure に興味があり実際始めるかもしれないという人は事前に二番目の記事を読むことをおすすめしたい。
結果
まず結果として、本を一通り読み終えた後に出力できる画像は以下のようなものができる。
物体を並べまくったり、
(近いところからの様子を生成した画像をミスって消してしまったので、明日の朝更新します。とりあえずアンチエイリアスしてないものを貼っておきます。)
(同日: 更新しました。物体の配置はランダムにしているので上のと違います。)
カメラを動かしたりできる。
まだ焦点ぼけの部分を実装していないので、年末のうちに作ってしまいたい。コードは Github - iyahoo/clj-ray-tracing に上げました。
レイトレーシングでは画像の書き出し部分以外はベクトルを中心とした数学的な演算が殆どなので、副作用を使わないプログラミングがしやすかった。関数型言語使ってみたいけど特に作るもの思いつかないなという人に個人的にオススメの題材に。
また今回使ったレイトレーシング本の続編が出ている (ただし未訳) ので、やりたくなったら続編も読もうと思う。 In One Weekend
Clojure に関して
プログラムは例えば以下のように C++ から Clojure に書き変えている。
int main() { int nx = 200; int ny = 100; std::cout << "P3\n" << nx << " " << ny << "\n255\n"; vec3 lower_left_corner(-2.0, -1.0, -1.0); vec3 horizontal(4.0, 0.0, 0.0); vec3 vertical(0.0, 2.0, 0.0); vec3 origin(0.0, 0.0, 0.0); hitable *list[2]; list[0] = new sphere(vec3(0, 0, -1), 0.5); list[1] = new sphere(vec3(0, -100.5, -1), 100); hitable *world = new hitable_list(list, 2); for (int j = ny - 1; j >= 0; j--) { for (int i = 0; i < nx; i++) { float u = float(i) / float(nx); float v = float(j) / float(ny); ray r(origin, lower_left_corner + u * horizontal + v * vertical); vec3 p = r.point_at_parameter(2.0); vec3 col = color(r, world); int ir = int(255.99 * col[0]); int ig = int(255.99 * col[1]); int ib = int(255.99 * col[2]); std::cout << ir << " " << ig << " " << ib << "\n"; } } }
;; 素直にそのまま Clojure に直したもの (defn header [nx ny] (str "P3\n" nx " " ny "\n255\n")) (defn int-color [f-color] (int (* 255.99 f-color))) (defn body [nx ny] (let [lower-left-corner (->Vec3 -2.0 -1.0 -1.0) horizontal (->Vec3 4.0 0.0 0.0) vertical (->Vec3 0.0 2.0 0.0) origin (->Vec3 0.0 0.0 0.0) sphere1 (->Sphere (->Vec3 0 0 -1) 0.5) sphere2 (->Sphere (->Vec3 0 -100.5 -1) 100) world (->Hitable-list (list sphere1 sphere2) 2)] (apply str (for [j (range (- ny 1) -1 -1) i (range 0 nx)] (let [u (/ i (float nx)) v (/ j (float ny)) r (->Ray origin (plus lower-left-corner (times u horizontal) (times v vertical))) p (point-at-parameter r 2.0) col (color r world) vs (vals col) [ir ig ib] (map int-color vs)] (str ir " " ig " " ib "\n"))))))
;; そこから関数を細かく切り出しシーケンス処理の連鎖にしたもの (defn header [nx ny] (str "P3\n" nx " " ny "\n255\n")) (defn int-color [f-colors] (map #(int (* 255.99 %)) f-colors)) (defn make-coordinates [nx ny] (for [j (range (- ny 1) -1 -1) i (range 0 nx)] [j i])) (defn coordinates-to-rate [[j i] ny nx] [(/ j (float ny)) (/ i (float nx))]) (defn make-ray [[v u] origin lower-left-corner horizontal vertical] (->Ray origin (plus lower-left-corner (times u horizontal) (times v vertical)))) (defn make-color [ray world] (color ray world)) (defn make-str [[ir ig ib]] (str ir " " ig " " ib "\n")) (defn make-world [] (let [sphere1 (->Sphere (->Vec3 0 0 -1) 0.5) sphere2 (->Sphere (->Vec3 0 -100.5 -1) 100) lis (vector sphere1 sphere2 sphere3 sphere4)] (->Hitable-list lis (count lis)))) (defn body [nx ny] (let [lower-left-corner (->Vec3 -2.0 -1.0 -1.0) horizontal (->Vec3 4.0 0.0 0.0) vertical (->Vec3 0.0 2.0 0.0) origin (->Vec3 0.0 0.0 0.0) world (make-world) allprocess #(-> % (coordinates-to-rate ny nx) (make-ray origin lower-left-corner horizontal vertical) (make-color world) vals int-color make-str)] (->> (make-coordinates nx ny) (map allprocess) (apply str))))
ここで allprocess
というのは座標のペア [x y]
を受け取り変換していって "r g b\n"
という PNM 画像の一行を構成する形に変換する関数ということになる。この様に一つのデータに対する処理としてまとめることで、map
のように渡した関数を並列に実行することができる pmap
などが使いやすく便利になる。pmap
はコア数 +2 の thread を立てるので、関数の処理が短いとオーバーヘッドの方が長くなってしまい早くならないらしい 参考:Understanding Clojure's Map & PMap と (source pmap)
。
以下はアンチエイリアスというものを導入後 (※上のコードの allprocess
の中に重い処理が入る)、allprocess
一回あたりに用する時間が非常に長くなった状態で (map allprocess)
を (pmap allprocess)
に変更した時の処理時間の変化で、それだけで半分以下になった。
;; 400x200 の画像で、 1 ピクセル辺りのレイの数が 100 の画像を作る in-one-weekend.core> (time (-main 400 200 100)) "Elapsed time: 902399.49433 msecs" nil in-one-weekend.core> (time (-main 400 200 100)) "Elapsed time: 434075.507751 msecs" nil
今回の例では1ピクセルで行われる計算が、他の場所での計算に全く影響を与えない/受けないので、簡単に並列化することができた。このように Clojure にあった書き方をすると簡単に恩恵を受けることができる。また多くの再帰関数はシーケンス処理の連鎖に変換できると考えて良いと思う。今回の場合は、あるデータの処理の結果が次のデータの処理に影響を与えるという場面では以外では再帰を使う必要がなかった。(今回コードでは、反射によっておこる色の変化を実装している color
と一番手前でぶつかる物体を見つけるための hitable-list
の hit
)
Clojure を書いていて疑問に思うこととして、defmulti/defmethod
などを利用してディスパッチを利用するべきかどうかの判断がわからないということがある。2年前に作ったプログラムは defmethod
などを使いまくって自分の書きたいように書いてしまっていたので、これが原因で遅かった可能性もある ((といっても使っていたのは Clojure 1.7.0 だったのであくまで要因の一つ。あとは pmap
の原理を理解せずに適当に使っていたということも原因だと思う)) 今回のプログラムを組んでいる時はレイトレーシングが処理の重いものであると最初から認識していたので、defmethod
を使ったディスパッチはあまり行わなかった。しかし行列演算でディスパッチを行わないうまい書き方がおもいつかなかったのでとりあえず使っていて、これが全体でかなり呼び出されているので、ここを修正できると早くなりそうではある。
上の疑問についてのヒントは Polymorphic performance が参考になった。この検証結果は 2015 年 4 月 (多分 1.7.0 の開発途中) のものであり、確かにマルチメソッドによるディスパッチを利用した関数呼び出しは他と比較すると遅い。しかし拡張のしやすさなどを考えると、この検証結果よりも更に高速化されている可能性が高い現在ならば、頻繁に呼び出す部分以外ではディスパッチを使っても良いのかなあと思えた。*3 しかしどうも defprecord
などで宣言したメンバ関数は cider で監視できないなどデバッグがしづらかったりした。
まだまだ元の C++ のコードに引きずられて関数とデータをうまく分離しきれておらず、コード全体で見るといまいち Clojure らしいコードになっていないのかなあと思う。この辺りは今後書きながら感覚を掴んでいきたい。
今後の Clojure-n
今回のことをベースにすれば様々なことができそうなのでいろいろやっていきたい。ひとまず質を上げるかどうかは気にせず物を作って、質を上げたくなったやつを適宜上げるという方向にしたい。
ClojureScript が書きたかったので Lifegame を作りここで公開する予定だったが間に合わなかったので今度作って上げる。
終わりに
動くものや目で見て楽しいものを作るということを全くしてこなかったので、今回選んだテーマは楽しくプログラミングできた。そもそも研究以外には言語仕様を学ぶ以上にあまりプログラミングをしてこなかったので、Clojure-n の活動を続けていきたい *4。
12 月はハチャメチャに忙しく、
新天地にアパートを探しに行き、
免許の更新をしに実家に帰り、
大学向けの研究の進捗を作りながら、
修士論文を書きながら、
SICP 読書会をやりながら、
御社での仕事へ向けた準備をしながら、
バイトで実験をしながらお偉いさんに自分の研究の意義を語り、
また別なバイトでは力仕事をしまくりながら、
ヅドベントネタ作りをした。今年 1 月 ~11 月に活動した総量以上にこの 12 月だけで動きまくった気がするので褒められまくりたい。
以上です。ここまで読んで下さりありがとうございました。良いお年を!
*1:漸近的型付けについては今年のアドベントカレンダーで見かけた 漸進的型付けの未来を考える という記事がわかりやすかった。この記事で説明されているキャストの仕組みは、 Typed Clojure も内部の処理に手を加えることはしないはずなので Clojure にも無いはず。
*2:ClojureScript について、厳密には別なコンパイラとなるためはライブラリではなく Clojure とは別な言語ということになると思うが、様々な Clojure の資産がそのまま利用できるようになっている。 Clojure と ClojureScript の違いは公式で詳しくまとめられている Differences from Clojrue
*3:Clojure の現在のバージョンは 12/08 で 1.9.0 に
*4:n は任意の整数