Clojure Web Development勉強 - ClojureScript(その4)figwheelの最小限設定
Figwheelは素晴らしい。FigwheelはClojureScriptを使うことで得る大きな喜びの一つだ。
と過剰に思われる売り込みで始める。しかし、個人的には全く誇張している意識はない。
Figwheelはwebsocketを使って、ソースファイルの変更を自動的にブラウザに反映させる開発ツールである。
前回lein cljsbuild autoでClojureScriptからJavaScriptへのコンパイルを自動化し、ソースファイルを変更して保存すれば自動的にJavaScriptが生成され、そのあとブラウザをリロードすることで変更を反映させることができる、というワークフローを紹介した。
Figwheelはそれをもう一歩進めた形になる。ClojureScriptのソースファイルを変更・保存した時点でブラウザリロードする必要なく自動的にブラウザに新しいJavaScriptがロードされる。
文章で説明しても凄さがいまいち伝わらない感があるが、動画で見た時は相当な衝撃を受けたのを覚えている。見たのはfigwheelの開発者Bruce Haumanによる紹介動画:
そして実際に使うとさらに感動する。「cljsbuildから一歩進んだ」というのは正しいが、その一歩がいかに重要かを充分伝えきれていないと思う。
まず、エディタから離れることなくブラウザで変更が確認できる。私の場合はvimを使っているのだが、:wと叩いた時点でブラウザに反映される。書いては:w、書いては:wでホームポジションから手を離すことなしに開発サイクルが回る。
そしてページリロードをしないことで状態が維持される。リロードした場合ページが初期状態に戻されてしまい、例えばそこから状態を色々変更していた場合、前回と同じ変更を加えていって手動でリロード前の状態に戻す必要が生じる。Figwheelを使うとその必要がなく、コード変更前のページの状態のまま挙動などが変わるのを確認できる。
今回のコード:
ポイントとなるのは3点。
- pluginsにlein-figwheelを追加する
- cljsbuildの:optimizationsがないビルドに:figwheel trueを追加する
- output-to/output-dirをresources/publicディレクトリのどこかにする
最初の二つはleiningenとcljsbuildにちゃんとfigwheelと連動するよう伝えるオプション。
三つ目のポイントは、figwheelが自動的に立ち上げてくれるローカルサーバがデフォルトでresources/pubicをルートディレクトリにしているので、サーバからアクセスするファイルはhtmlであれjavascriptであれすべてresources/publicの下位ディレクトリ(あるいはresources/public内)になくてはいけない。
これでlein figwheel
とコマンドを走らせると
Figwheel: Cutting some fruit, just a sec ... Figwheel: Validating the configuration found in project.clj Figwheel: Configuration Valid :) Figwheel: Starting server at http://0.0.0.0:3449 Figwheel: Watching build - dev Figwheel: Cleaning build - dev Compiling "resources/public/js/main.js" from ["src"]... Successfully compiled "resources/public/js/main.js" in 11.823 seconds. Launching ClojureScript REPL for build: dev Figwheel Controls: (stop-autobuild) ;; stops Figwheel autobuilder (start-autobuild [id ...]) ;; starts autobuilder focused on optional ids (switch-to-build id ...) ;; switches autobuilder to different build (reset-autobuild) ;; stops, cleans, and starts autobuilder (reload-config) ;; reloads build config and resets autobuild (build-once [id ...]) ;; builds source one time (clean-builds [id ..]) ;; deletes compiled cljs target files (print-config [id ...]) ;; prints out build configurations (fig-status) ;; displays current state of system Switch REPL build focus: :cljs/quit ;; allows you to switch REPL to another build Docs: (doc function-name-here) Exit: Control+C or :cljs/quit Results: Stored in vars *1, *2, *3, *e holds last exception object Prompt will show when Figwheel connects to your application
こんな感じのメッセージが出力される。
あとはブラウザからlocalhost:3449
にアクセスするとindex.htmlが開き、コンパイルされたJavaScriptが走る。
ChromeのJavaScriptコンソールなどを立ち上げると
Figwheel: trying to open cljs reload socket utils.cljs?rel=1478607372893:49 Figwheel: socket connection established utils.cljs?rel=1478607372893:49
とソケットで通信していることがわかる。
これでcore.cljsをいじって保存してみる。例えば
(ns min-figwheel.core) (js/alert "Hello ClojureScript!")
を
(ns min-figwheel.core) (js/alert "Hello Figwheel!")
に変更してセーブする。それだけで自動的に新しいアラートメッセージがブラウザ上で表示される。
あるいは
(ns min-figwheel.core) (js/console.log "Hello Figwheel!")
と変更してコンソールを確認すると
Figwheel: notified of file changes utils.cljs?rel=1478607372893:49 Hello ClojureScript! core.cljs?rel=1478641111736:2 Figwheel: loaded these files utils.cljs?rel=1478607372893:49 ("../min_figwheel/core.js") utils.cljs?rel=1478607372893:51
こんな風に表示される。ちゃんとソースの変更が反映されてコンソール出力できているのがわかる。core.cljsのどこに対応しているのかまで表示していて、リンクをクリックするとソースが確認できるのもなかなか面白い。
さて、今まで恐ろしく簡単なClojureScript機能しか使ってきていないので、Figwheelのありがたみがまだ明確に出てきていない。Figwheelの真価が発揮されるのはClojureScriptで複雑な処理を書き出してからである。特に、FigwheelとDOM操作系のライブラリを合わせることでページをグリグリと非常にインタラクティブに作成できるようになる。
次はClojureScript上でどうやってDOM操作をしていくかという観点からReactについて書きたい。
Clojure Web Development勉強 - ClojureScript(その3)cljsbuildオプションあれこれ
cljsbuildには様々なオプションがある。なんでこんなにあるの?と言いたくなるくらいある。
個人的に使いそうだなぁと思うものを抜粋すると:
:cljsbuild { :builds { :main { :source-paths ["src-cljs"] :compiler { :optimizations :whitespace :output-to "resources/public/js/main.js" :output-dir "target/my-compiler-output-" :source-map "resources/public/js/main.js.map" :externs ["jquery-externs.js"] :libs ["closure/library/third_party/closure"] :foreign-libs [{:file "http://example.com/remote.js" :provides ["my.example"]}]}}
こんなところだろうか。
最後の4つについて簡単に紹介すると:source-mapでブラウザ上から実行されているJavaScriptのソースを確認でき、デバッグなどに非常に便利(らしい)、:externs、:libs、:foreign-libsの三つを記述することでコンパイルされたJavaScriptがさらにminifyされても外部ライブラリが使用できるように設定できる。後々より詳細に解説したい。
今回特に集中的に説明したいのが、Google Closure Compilerを使ったJavaScriptのMinificationのオプションである:optimizationsの詳細と、:buildsの値を前回のようなvectorではなくmapにすることで複数のビルド設定を指定し、開発環境と本番環境とのビルドを区別する方法である。
Google Closure Compilerと:optimizationsオプション
ここ十年間で起きたJavaScriptの隆盛の中心にGoogleがいるのは間違いない。Google MailとGoogle Mapsという二大ajaxアプリを引っ提げて00年代中盤のソフトウェア界に衝撃を与えたのももう十年も昔の話だ。そしてJavaScriptエンジンのV8やそれを搭載して開発環境(というのは言い過ぎにしても)としても優れているChromeを発表し、「重いしセキュリティ的にも心配だからJavaScript切っとこう」という態度を過去のものにした。
そのGoogleが自社のJavaScript開発用のツールとしてOSS化したのがGoogle Closure Toolsである。Compiler、Library、Templates、Stylesheets、Linterの5つのパーツからなるこのツールセットは、標準ライブラリを持たないJavaScriptにとって高品質な共通ライブラリを提供し得るものとなっている。(正直そのポジションの最も美味しい部分はJQueryに食われたけど)
ClojureScriptはGoogle Closure Toolsのうち、Google Closure LibraryとGoogle Closure Compilerを根本的なツールとして組み込んでいる。その二つを使うことによって複雑な処理を行うコードでもかなりバイト数を絞り込み、トラフィックもロード時間も抑え込むことが可能となっている。ただし、その結果コンパイルされたJavaScriptコードが難読化されバグがわかりにくくなったりすることと、そもそもコンパイルに数秒かかるので開発中にフローが中断されてしまう、などという難点がある。
そこで開発中はMinificationは切っておいて分かりやすいJavaScriptアウトプットを素早く作成し、本番にデプロイする時点でGoogle Closure Compilerの全力を出してもらう、という流れになる。そのコントロールのための設定が:optimizationsであり、:none、:whitespace、:simpleそして:advancedの四つの値のいずれかを取る。
この四つのオプションについての詳細は以下の記事がよくまとめている:
ClojureScript optimizations on Node - huh?
:none
Google Closure Compilerを全く使わない。ClojureScriptコンパイラが吐くJavaScriptをそのまま使っている。その結果、ファイルサイズが大きくなっていることと、Google Closure Libraryへのdependencyや自分の書いたコードの呼び出しが一つのファイルで完結しないため、HTMLドキュメントに明示的に記述しなければいけない。具体的に言うと前回の例でも出てきた
<html> <body> <script src="out/goog/base.js"></script> <script src="out/main.js"></script> <script>goog.require('my_code.core')</script> </body> </html>
の三行目と五行目は:optimizations :noneの時にだけ必要なものである。
:whitespace
このオプションからGoogle Closure Compilerが使われだす。この時点では、構文上必要ない空白や括弧などを自動的に排除して、dependencyなどを全部一つのファイルにまとめてくれる。なのでHTMLは以下のように記述できる:
<html> <body> <script src="out/main.js"></script> </body> </html>
ちなみにこの記事の頭でリンクしたこのサンプルコードのコメントに
; Defaults to :whitespace.
とあるが、現状では明らかに間違いで:optimizationsの指定がない場合は:noneにデフォルトしている。
:simple
:whitespaceの時の最適化に加え、関数内のローカル変数のシンボルを短くする。
:advanced
:simpleの最適化に加え、かなり突っ込んだコードの変形を行う。あらゆるシンボルを短くし、コードのインライン化なども行い、さらにはコードの中から使われていない部分を探し出してバッサリ削除するというDead Code Eliminationも行う。最後のものは、サード・パーティ・ライブラリなどを使っていると特に素晴らしい効果を発揮する。しかし、そういった激しい最適化が行われている分、外部ライブラリなどもGoogle Closure Compilerに対応しているか、こちらで最低限対応するようExternなどの設定をしてやらないと、シンボルが正しく参照されなかったりなどと問題を引き起こす。
開発環境では:none、本番環境用には:advanced(外部ライブラリなどが許せば・・・)というのが望ましい。
複数のビルド設定の管理
というわけで少なくとも二つ(あるいは例えばテスト環境も入れたければ三つ)のビルド設定が望ましいわけだが、いちいちproject.cljを書き換えるよりもどちらの設定もproject.cljに記述してcljsbuildにどの設定を使うか指定して実行させたい。
前回は以下のように:
:cljsbuild {:builds [{:source-paths ["src"] ...
と:buildsの値をvectorにしてあったのだが、それをmapに変え、キーを設定名、値を設定にしてやる。
:cljsbuild {:builds {:dev {:source-paths ["src"] :compiler {:output-to "devout/main.js" :output-dir "devout"}} :prod {:source-paths ["src"] :compiler {:output-to "out/main.js" :output-dir "out" :optimizations :advanced}}}}
コンパイルするには、
lein cljsbuild once dev
あるいは
lein cljsbuild once prod
とコマンドの末尾に設定名を指定するだけ。
:dev設定だと:optimizationsはなしで前回と同じアウトプットがdevoutディレクトリに出力される。
:prod設定だと思いっきり最適化されたJavaScriptがoutディレクトリに出力される。out内のディレクトリ構成も:
out ├── cljs │ ├── core.cljs │ └── core.js ├── constants_table.js ├── main.js └── try_cljsbuild └── core.js
とかなり簡単になっているのがわかる。しかし、コンパイルにかかる時間は大体十倍ぐらいの違いが出る。
自動コンパイル
最後に軽く自動コンパイルについて触れておきたい。
今まで
lein cljsbuild once dev
などとやって一回ずつコンパイルしてきた。しかし開発を進めていく上でできればそういう手動なステップは最低限に留めておきたい。具体的にはsrcディレクトリ内のcljsファイルに変更を加えて保存した時点で自動的にコンパイルが行われるようにしたい。そのためにはbashから以下のコマンドを走らせる:
lein cljsbuild auto dev
そうすると以下のメッセージが表示される:
Watching for changes before compiling ClojureScript...
cljsbuildが走っている端末はそのまま放置して、他のウィンドウでcore.cljsをアップデートすると:
Compiling "devout/main.js" from ["src"]...
Successfully compiled "devout/main.js" in 0.772 seconds.
と先ほどのウィンドウで自動的にコンパイルが行われたのが通知される。そしてブラウザをリロードすれば、新しいコードが実行される。これで1ステップ省略でき、よりストレスフリーでフィードバックループが直接的な開発環境に近づいた。
しかしまだブラウザのリロードという邪魔な手順がある。次回はそれを省いてくれるツールfigwheelについて解説する。
今回のコード:
Clojure Web Development勉強 - ClojureScript(その2)cljsbuildでコンパイル
Clojureビルドツールあれこれ
Clojureビルドツール界隈ではleiningenが圧倒的シェアを誇っておりそこにbootが食らいついている、というような状況になっている。
私はbootは使ったことはないが、leiningenにはいつも非常にお世話になっている。project.cljに依存ライブラリなども含めたビルドオプションを記述して、あとはライブラリのダウンロードからコンパイルから実行までleinコマンドでできてしまうのは本当に便利だ。
前回のClojureScriptのコンパイルではleiningenもbootも出る幕がなく、自分でClojureScriptコンパイラのjarを落としてきてjavaコマンドで実行していた。しかし、他のライブラリへの依存性管理や、より便利なワークフローツール(特にfigwheel!)の利用などのためにも、ビルドツールとしてleinを使っていきたい。
普段はプロジェクトの雛形すらlein new <project-name>
あるいはlein new reagent <project-name>
といったコマンドでleiningenに用意してもらうのだが、今回はそこは手動でやって最低限のプロジェクトの条件を見ていく。実は似たようなことをQiitaでもやったが、その後いろいろと試していくうちに知見も溜まったので・・・
ClojureScript/cljsbuild用のproject.clj
用意するディレクトリとファイルは以下の通り:
. ├── index.html ├── project.clj └── src └── min_cljsbuild └── core.cljs
src/min_cljsbuild/core.cljsとindex.htmlは前回でもおなじみ。内容も前回と全く同じ。
ClojureScriptコンパイラcljs.jarとビルドのために実行するスクリプトであるbuild.cljが消え、そのかわりleiningenがビルドに必要な情報を記述したproject.cljが加わっている。
本当の最低限
いろいろと試した結果、cljsbuildがエラーを吐かずに「動作」するproject.cljは以下の通り:
(defproject min-cljsbuild "0.0.1" :dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/clojurescript "1.9.293"]] :plugins [[lein-cljsbuild "1.1.4"]] :cljsbuild {:builds []})
上記のようなproject.cljを置いて、bashでlein cljsbuild once
とコマンドを走らせると
Compiling ClojureScript...
とメッセージが表示される。その後ディレクトリを調べると、いろんなファイルが出力されているのがわかる。
. ├── dev-resources ├── index.html ├── project.clj ├── resources ├── src │ └── min_cljsbuild │ └── core.cljs ├── target │ ├── classes │ │ └── META-INF │ │ └── maven │ │ └── min-cljsbuild │ │ └── min-cljsbuild │ │ └── pom.properties │ ├── cljsbuild-crossover │ └── stale │ └── extract-native.dependencies └── test
しかし肝心のJavaScriptファイルがない。というのもcljsbuildに「どこにコンパイルしてほしいClojureScriptファイルが入っているのか」という情報を渡していなかったため、単にテンプレ的に必要なファイルやディレクトリを作成したのみで終わってしまったわけだ。
なので「コンパイルしてほしいClojureScriptはsrcディレクトリに入っていますよー」という情報を渡してやる。
JavaScriptが出力される最低限
(defproject min-cljsbuild "0.0.2" :dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/clojurescript "1.9.293"]] :plugins [[lein-cljsbuild "1.1.4"]] :cljsbuild {:builds [{:source-paths ["src"]}]}) ;;ここ
まずは先ほどcljsbuildが作成したディレクトリを全部削除してしまう
rm -rf dev-resources;rm -rf test;rm -rf resources;rm -rf target
そしてもう一回実行:
lein cljsbuild once
そうするとこんなメッセージが出て:
Compiling ClojureScript...
Compiling "/min-cljsbuild/target/cljsbuild-main.js" from ["src"]...
Successfully compiled "/min-cljsbuild/target/cljsbuild-main.js" in 0.82 seconds.
こんなディレクトリ構成が出力される:
. ├── dev-resources ├── index.html ├── project.clj ├── resources ├── src │ └── min_cljsbuild │ └── core.cljs ├── target │ ├── classes │ │ └── META-INF │ │ └── maven │ │ └── min-cljsbuild │ │ └── min-cljsbuild │ │ └── pom.properties │ ├── cljsbuild-compiler-0 │ │ ├── cljs │ │ │ ├── core.cljs │ │ │ ├── core.js │ │ │ └── core.js.map │ │ ├── goog │ │ │ ├── array │ │ │ │ └── array.js │ │ │ ├── asserts │ │ │ │ └── asserts.js │ │ │ ├── base.js │ │ │ ├── debug │ │ │ │ └── error.js │ │ │ ├── deps.js │ │ │ ├── dom │ │ │ │ └── nodetype.js │ │ │ ├── math │ │ │ │ ├── integer.js │ │ │ │ └── long.js │ │ │ ├── object │ │ │ │ └── object.js │ │ │ ├── reflect │ │ │ │ └── reflect.js │ │ │ └── string │ │ │ ├── string.js │ │ │ └── stringbuffer.js │ │ └── min_cljsbuild │ │ ├── core.cljs │ │ ├── core.cljs.cache.edn │ │ ├── core.js │ │ └── core.js.map │ ├── cljsbuild-crossover │ ├── cljsbuild-main.js │ └── stale │ └── extract-native.dependencies └── test
前回見たようなJavaScriptファイルがtargetディレクトリに入っている。とくに重要なのがtarget/cljsbuild-compiler-0/goog/base.jsとtarget/cljsbuild-main.jsの2ファイルで、これが前回のout/goog/base.jsとout/main.jsに相当する。
ただ、target/cljsbuild-compiler-0/googといったディレクトリパスも「コンパイル中一時的な置き場として名前衝突が起きないようつけました」感満載だしcljsbuild-main.jsと最終的にロードするJavaScriptファイルにコンパイラツールの名前が付いているのもちょっとダサい。
ということで、後もう一歩、出力ファイルの名前とパスをproject.cljに記述する。
出力先を指定したproject.clj
(defproject min-cljsbuild "0.0.3" :dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/clojurescript "1.9.293"]] :plugins [[lein-cljsbuild "1.1.4"]] :cljsbuild {:builds [{:source-paths ["src"] :compiler {:output-to "out/main.js" ;;ここ :output-dir "out"}}]})
コンパイラにmain.jsの出力先と名前、そしてgoogを含めた必要とされるJavaScriptライブラリの出力先となるディレクトリを指定している。
後は先ほどと同じく、cljsbuildに作成されたディレクトリを削除してからlein cljsbuild once
を実行する。
. ├── dev-resources ├── index.html ├── out │ ├── cljs │ │ ├── core.cljs │ │ ├── core.js │ │ └── core.js.map │ ├── goog │ │ ├── array │ │ │ └── array.js │ │ ├── asserts │ │ │ └── asserts.js │ │ ├── base.js │ │ ├── debug │ │ │ └── error.js │ │ ├── deps.js │ │ ├── dom │ │ │ └── nodetype.js │ │ ├── math │ │ │ ├── integer.js │ │ │ └── long.js │ │ ├── object │ │ │ └── object.js │ │ ├── reflect │ │ │ └── reflect.js │ │ └── string │ │ ├── string.js │ │ └── stringbuffer.js │ ├── main.js │ └── min_cljsbuild │ ├── core.cljs │ ├── core.cljs.cache.edn │ ├── core.js │ └── core.js.map ├── project.clj ├── resources ├── src │ └── min_cljsbuild │ └── core.cljs ├── target │ ├── classes │ │ └── META-INF │ │ └── maven │ │ └── min-cljsbuild │ │ └── min-cljsbuild │ │ └── pom.properties │ ├── cljsbuild-crossover │ └── stale │ └── extract-native.dependencies └── test
となって、手動でcljs.jarを使った時と概ね似た構成になる。違いはdev-resources、resources、testという空ディレクトリと、コンパイル時の一時ファイルの残りであるtargetディレクトリで、これらは挙動に何ら影響しない。この時点では削除してしまっても構わないくらいである。
あとは前回と同じindex.htmlを開けば同じアラートが表示される。
今回のコード:
次回はcljsbuildのオプションを少し調べていきたい。
Clojure Web Development勉強 - Clojurescript(その1)cljs.jarでコンパイル
一旦サーバサイドを離れて、フロントエンドの開発を進めてみる。
フロントエンドについては以前から少し試していて、拙いながらも渋谷のLisp.Meetupでもライブコーディングしながらプレゼンしたことがある。その時はClojurescriptからFigwheel、Reagent、d3.jsなどと結構いろんな部分に言及できた(その分話が散漫になったきらいはあるが・・・)。
初心に戻ってもう一度簡単なところから調べていきたい。
というわけで、生のclojurescriptをコンパイルするところから。
公式サイトの上記のドキュメントをタネに、最低限のClojurescriptを書いてコンパイルして表示してみる。
そのコードというのはこれ:
(ns manual_clojurescript.core) (js/alert "Hello Clojurescript!")
名前空間の定義と、ブラウザのアラート機能で"Hello Clojurescript!"と表示させるだけ。js/alert
はClojurescriptからJavaScriptの機能を呼び出している。とりあえずこれをcore.cljsとしておく。Clojurescriptなので.cljsである。(cljcの話は違う記事で言及する)
さて、コードを書いたはいいが、これをどうすればいいのか。以下の三つのステップが必要になる。
- ClojurescriptをJavaScriptにコンパイル
- JavaScriptをロードするHTMLドキュメントを作成
- HTMLドキュメントをブラウザで表示
そのために幾つかサポートのためのファイルとディレクトリ構成を設定する。最終的に以下のような構成になる。
. ├── build.clj ├── cljs.jar ├── index.html └── src └── manual_clojurescript └── core.cljs
core.cljsがsrc/manual_clojurescript/のディレクトリの下にあるに注意。それ以外のファイルは全てディレクトリのトップレベルにある。
ClojurescriptをJavaScriptにコンパイル
さて、まずステップ1のために必要なcljs.jarとbuild.cljについて。
cljs.jarはQuick Startに載っているリンクから落とせる、Javaで書かれているClojurescript->Javascriptコンパイラである。
そのコンパイラに出す指示を入れたファイルがbuild.cljで、内容は以下のようになっている。
(require 'cljs.build.api) (cljs.build.api/build "src" {:output-to "out/main.js"})
srcディレクトリに含まれているcljsファイルをJSにコンパイルして、まだ未作成のoutディレクトリのmain.jsファイルに出力してくれ、という内容。
src/manual_clojurescript/core.cljs、build.cljそしてcljs.jarの三つが揃えばステップ1のコンパイルができる。bashから
で、outというディレクトリにmain.jsやその他もろもろが作成されるはずだ。構成としてはこんな感じ:
. ├── build.clj ├── cljs.jar ├── index.html ├── out │ ├── cljs │ │ ├── core.cljs │ │ ├── core.js │ │ └── core.js.map │ ├── goog │ │ ├── array │ │ │ └── array.js │ │ ├── asserts │ │ │ └── asserts.js │ │ ├── base.js │ │ ├── debug │ │ │ └── error.js │ │ ├── deps.js │ │ ├── dom │ │ │ └── nodetype.js │ │ ├── math │ │ │ ├── integer.js │ │ │ └── long.js │ │ ├── object │ │ │ └── object.js │ │ ├── reflect │ │ │ └── reflect.js │ │ └── string │ │ ├── string.js │ │ └── stringbuffer.js │ ├── main.js │ └── manual_clojurescript │ ├── core.cljs │ ├── core.cljs.cache.json │ ├── core.js │ └── core.js.map └── src └── manual_clojurescript └── core.cljs
outに見えるファイルの概要は以下のとおり:
- manual_clojurescriptディレクトリ:srcに含まれていた自前のclojurescriptコードのコンパイル先
- cljsディレクトリ:clojurescript自体の言語機能やライブラリが含まれている
- googディレクトリ:Clojurescriptが乗っかっているGoogleのオープンソースJavaScriptライブラリであるGoogle Clojure Libraryの出力先
- main.js:上記のディレクトリに含まれているJSをつなぎ合わせるコード
JavaScriptをロードするHTMLドキュメントを作成
さて、次はこのコンパイルされたJavaScriptのロード先であるHTMLドキュメント、index.htmlを作成する。今回は最低限ということで以下のようなファイルになる:
<html> <body> <script type="text/javascript" src="./out/goog/base.js"></script> <script type="text/javascript" src="./out/main.js"></script> <script type="text/javascript">goog.require("manual_clojurescript.core");</script> </body> </html>
まずGoogle Closure Libraryの便利機能を裏で色々使うので、それをgoog/base.jsを通じてロードしておく。
次に自分の書いたコードも呼び出せるようmain.jsをロード。
最後に、Google Closure Libraryのgoog.requireを使って自分の書いたClojurescriptのコンパイル先であるmanual_clojure.coreを実行。これで、core.cljsに記述したロジックに従って処理が行われる。
HTMLドキュメントをブラウザで表示
それでは実際に走らせてみる。
一番手軽なのはbashから
open ./index.html
でローカルファイルとして開いてしまうこと。これでちゃんとアラートポップアップに"Hello Clojurescript!"と表示されるはず。
もうちょっとちゃんとローカルサーバ立ててみたい場合は:
python -m http.server
あるいは
python -m SimpleHTTPServer
(前者はPython 3.x、後者はPython 2.xの場合)
のようにサーバ立ててブラウザでlocalhost:8000などと見てもオッケー。
とりあえず最低限のClojurescriptを書いてコンパイルしてロードするHTML用意して開くところまでは来れた。次はlein-cljsbuildを使ったコンパイルプロセスの簡略化と自動化。
Clojure機械学習勉強 - Gorilla REPLと線形回帰 (その2)
昨日のコードを2点修正:
- 前回は独立変数を一つだけ用意したが、今回は二つ。さらに、今後独立変数の数の増減はX値と母数のθ値だけアップデートすれば、他のコードは変更なしで機能するようリファクタした。
- core.matrixの実装を、標準実装のndarray、vectorz-cljそしてclatrixの三種類から選べるようにした。
そのコードがこれ:
一番重い処理が、30000回更新したθ値を求める以下のコード:
(doall (theta-after-n-iteration 30000))
まずはndarrayで実行してみた場合:
"Elapsed time: 16456.788619 msecs" [2.0190228761901188 5.246421586567178 2.879450293240761] (抜粋)
vectorz:
"Elapsed time: 2122.331857 msecs" [1.881619902630865,4.979437022948518,2.8889278718090208]
頭の方で
(m/set-current-implementation :vectorz)
と指定するだけで十倍近く早くなっている。
そしてclatrix。なんと全く同じコードで:
Exception thrown: clojure.lang.ExceptionInfo (Can't broadcast to a lower dimensional shape)
と例外投げて死んでくれる。
多分clatrixだと(* A B)
といった処理はAの次元数がB以下(というかAがスカラー値かベクトルかBと全く同じ次元である)必要があるのに対して、他の実装ではBがスカラーかベクトルで他の次元が合えば、BをAに合わせてbroadcastしてくれていたようだ。(多分。逆かな?)
同じcore.matrixのAPIを実装しているはずなのに、こういうところで違いが出るのは面白くもあり、めんどくさくもあり。少なくとも「APIが同じだから入れ替えは一行で済むよ!」というのは盛りすぎなようである。
Clojure機械学習勉強 - Gorilla REPLと線形回帰
まあ何も言わずこのGorilla-replのOnline Viewerを見てくれ。
コードの常であるが、「どうやって正しく計算するか」よりも「どうやって正しく読み手にコードの意図を伝えるか」が最も難しかった。
一番腐心したところの割には、正直あまり成功しているとは言い難いが・・・ とりあえずLaTeXで苦心したりplotの機能を探ったりしながら作ってみた。今後はもうちょっと何とかなりそうな気がする。
次はcore.matrixのAPIを実装した高速行列計算ライブラリであるVectorzとClatrixの話をしたい。
追記:うっかり乱数のシードを同じにしてしまった。いかんいかん。というか、正直な話、乱数のシードはグローバルで一括管理したいよなぁ・・・
Clojure機械学習勉強 - core.matrix(その1)
前置き
Clojure Advent Calendarで機械学習ネタで記事を書くことを宣言してしまった。どうしよう。
C++、Python、Matlab\Octaveあたりではちょこちょこと(業務も含めて)機械学習はやったことがあるが、Clojureでは「JVMだし数値計算はしたくないよねー」とlisp.meetupの懇親会でネタにしまくっていたというのに。
機械学習なんて大量のデータを行列に投げ込んで出来るだけ早くぶん回すような世界なんだから、C++(かFortran!)コードを呼び出して行列データ構造で計算するのが正しいだろうし、それってJVMでしかも関数型な言語でやるのは筋悪なんじゃないか?というのが直感的な意見だし、lisp.meetupの他の参加者からも概ね同意を得ていた。*1
まあ多分実際にその通りなのだが、そこはほら、ものは試しということで、Clojureでどこまでやれるのかを調べてみたい。
あと他言語で覚えたアルゴリズムをClojureで実装してみると作法が大きく違うので理解が進む(気がする)ので、機械学習の勉強にもなるはず。
というわけで、まずはClojureにおける行列計算のAPIであるcore.matrixを見てみたい。
core.matrix
Clojureには複数の行列計算用ライブラリがある。純粋なJVM実装であるvectorz、JavaからBLASを呼び出すjblasを使っているclatrix、そしてJNIで直接BLAS系のライブラリであるATLASを呼び出しているNeanderthalあたりが強いようだ。NeanderthalはとくにGPUなども非常に簡単にアクセスできるっぽい。
それらのライブラリの上に共通のAPIとして存在するのがcore.matrixである。Neanderthalは以前は対応してなかったみたいだが、core.matrixの対応済みライブラリのリストが最近アップデートされた時に加えられたので対応したみたいだ。
とりあえずvectorzなどのちゃんとした行列計算用ライブラリを使わず、core.matrixの標準実装(clojure自体のデータ構造をwrapしているだけなので遅い)だけでAPIに慣れ親しんでみる。
まずproject.clj:
(defproject corem "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.8.0"] [net.mikera/core.matrix "0.56.0"]])
そしてとりあえずインポート:
(ns corem.core (:refer-clojure :exclude [* - + == / < <= > >= not= = min max]) (require [clojure.core.matrix :as m] [clojure.core.matrix.operators :refer :all] ))
*
や+
などの演算子をclojure.core.matrix.operatorsのものでオーバーライドしている!が普通の数字もちゃんとオーバーライドされたもので計算できるので大丈夫(らしい)。実行速度に影響出ないんだろうか。後々試してみたい。とりあえずこれで一般的な行列計算的なことができる。
(def a (m/matrix [[1 2 3] [4 5 6]])) a ;; [[1 2 3] [4 5 6]] (m/shape a) ;; [2 3] (m/ecount a) ;; 6 (m/esum a) ;; 21 (m/transpose a) ;; [[1 4] [2 5] [3 6]]
行列の行数・列数を返すshape、成分の数を返すecount、成分の和を返すesumそして転置行列を返すtransposeあたりは基礎中の基礎だろう。
関数型ならではの高階関数もある:
(m/emap inc a) ;; [[2 3 4] [5 6 7]] (m/ereduce * a) ;; 720
行列とスカラー値の四則演算は簡単:
(+ a 1) ;; [[2 3 4] [5 6 7]] (- a 1) ;; [[0 1 2] [3 4 5]] (* a 2) ;; [[2 4 6] [8 10 12]] (/ a 2) ;; [[1/2 1N 3/2] [2N 5/2 3N]] (/ a 2.0) ;; [[0.5 1.0 1.5] [2.0 2.5 3.0]]
最後、割り算のところだけは、とくに速度重視の場合は気をつけないといけない・・・ 浮動小数点と合理数では効率があまりにも違いすぎる。
同じ形の行列とも四則演算は簡単:
(def b (m/matrix [[2 2 2] [2 2 2]])) (+ a b) ;; [[3 4 5] [6 7 8]] (- a b) ;; [[-1 0 1] [2 3 4]] (* a b) ;; [[2 4 6] [8 10 12]] (/ a b) ;; [[1/2 1N 3/2] [2N 5/2 3N]]
行列と、列の数が一致する行ベクトルとの四則演算では(本当はスカラー値の場合も)broadcastingという計算が働き、行列にある行の数だけ同じ行ベクトルが並んでいるものと看做される。
(def c (m/matrix [-1 0 1])) (+ a c) ;; [[0 2 4] [3 5 7]] (- a c) ;; [[2 2 2] [5 5 5]] (* a c) ;; [[-1 0 3] [-4 0 6]]
割り算は0が入っていなければ大丈夫。
行の数が一致する列ベクトルとは残念ながらbroadcastは作動しない。(transpose (+ (transpose a) (transpose d)))
などと二重に転置をしてやる必要がある。
ここらへんで一旦終わる。次は線形代数的な機能と、簡単に線形最小二乗法でもやってみたい。
参考文献的なもの
公式wiki - Home · mikera/core.matrix Wiki · GitHub
API Guide - Core.matrix 0.44.0
core.matrixの作者であるMichael Andersonによる2014年のClojure Conjでのトークは今回書いた内容あたりも含めていろいろと説明していてなかなか良かった。