ClojureScript でボイド(Boids)なプログラムを書いてみる

はじめに

ClojureScript の勉強のために自分で書いてみたいコードを探していたところ、以下の記事を発見。

【ボイド】JavaScriptとHTML5で『群れ』をシミュレーションしてみよう【プログラミング】 - あのねノート。

JavaScript → ClojureScript に書き直してみることにしました。

作るもの

ボイド(Boids)という人工生命シュミレーションプログラムを作ります。詳しくは元記事を参考にしてください。

完成品はこんな感じとなります。ソースはこちら

プログラミングの下準備

ClojureScript で作成するにあたり、leiningenlein-cljsbuild を活用します。

  • Leiningen: Clojure(ClojureScriptを含む)のプロジェクトを便利に管理するためのツール。
  • lein-cljsbuild: Leiningen のプラグイン。 これを使うことにより ClojureScript のコンパイルを自動で行える。

現状、ClojureScript で何かを作るならこの二つはデファクトなんで何も考えずに利用しましょう。

プロジェクト作成

lein new cljs-boids

Leiningen の lein コマンドでプロジェクトのひな形を作成します。

project.clj へ記述を追加

(defproject boids "0.1.0-SNAPSHOT"
  :description "Sample program of Boids written by ClojureScript"
  :url "https://github.com/snufkon/clj-boids"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :source-paths ["src/cljs"]
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [org.clojure/clojurescript "0.0-2069"]]
  :plugins [[lein-cljsbuild "1.0.0"]]
  :cljsbuild {:builds
              [{:source-paths ["src/cljs"]
                :compiler {:output-to "resources/public/js/main.js"
                           :optimizations :advanced}}]})
  • :dependenciesclojurescriptを追加。
  • :pluginslein-cljsbuildを追加。
  • :cljsbuildを追加。
    • source-pathsソースコード(main.cljs)を置くsrc/cljsを指定。
    • compilemain.cljsコンパイルしてできたjsファイルの出力先(output-to)として、resources/public/js/main.jsを追加。
    • compileに最適化オプション(optimizations)として、advancedを指定。

コンパイルオプション

ちょっとだけ補足しておきますが、ボイドのプログラム作成には関係ないので読まなくても良いです。

コンパイルのオプションに:advancedを指定していますが、開発中はデバッグのため

:optimizations :whitespace
:pretty-print true

を指定しています。このオプション指定だとコンパイル後に出力されるmain.jsのコードが読める形で出力されます。 :advancedオプションだと、強力な最適化を行って、コードサイズや実行にかかる時間を少なくしてくれますが、出力されたコードは読めません(頑張れば読めるのかも...)。ちなみに、以下のようなことをしてくれているみたいです。

  • 変数や関数の名前を短いものにリネームする(mungingと呼ばれる)。
  • オブジェクトのネストを平板化する。
  • 未使用のコードを消去する。
  • インライン関数を作成する。
  • JavaScriptランタイムの特性に沿ってパフォーマンスを最適化する。

以下の本より引用。

ClojureScript: Up and Running

ClojureScript: Up and Running

:advancedコンパイルする際、いくつかの前提を元にコードを処理するため、処理対象のソースコードがこの前提に沿っていないと出力されたコードが期待する動作とならない場合もあることを頭に入れておきましょう。開発の途中途中、たまに:advancedコンパイル&実行して動作確認しておくのが良いと思います。プログラム完成後、:advancedコンパイル&実行、動かない...とならないように気をつけましょう(自戒を込めて...)。

index.html の作成

元記事index.htmlをそのまま、resources/public/index.htmlにコピーでおしまい。

main.cljs の作成

コード全体は、こちらで確認してください。気になる部分だけ解説しておきます。

(def boids (array))

(defn make-boids []
  (loop [i 0]
    (when (< i NUM_BOIDS)
      (aset boids i (js-obj "x" (* (js/Math.random) SCREEN_SIZE)
                            "y" (* (js/Math.random) SCREEN_SIZE)
                            "vx" 0
                            "vy" 0))
      (recur (inc i)))))

(make-boids)

boidsarrayjs-objで表現します。はじめ、VectorMapで表現してプログラムを作ったんですが、元のJavaScriptのコードと比較してあまりにも遅かったため書き直しました。(StackOverflowで質問したらarrayを使えと)

arrayのデータへの取得、更新にはaget,asetをそれぞれ利用します。

(aget boids index "x")  ;; boids[index].x
(aset boids index 10)   ;; boids[index] = 10
(set! (.-onload js/window) init)

set!を使い、window.onloadinit関数を設定します。

(js* "debugger;")

js*を利用してJavaScriptのコードを直接埋め込むことができます。完成したプログラムには記載していませんが、デバッグ時に上記のコードを活用していました。

(.time js/console "label name")
;; 計測したい処理を記述
(.timeEnd js/console "label name")

こちらも完成したプログラムに記載していませんが、処理時間を計測するために上記コードを活用していました。

おわりに

ClojureScript でプログラムを書くのに慣れていないので思ったより時間がかかりました。最初にboidsVector&Map で表現して作成するまでは良かったんですが、実行速度に問題があることに気づき、処理時間を計測したり、boidsの表現をarrayに変えたり、完成して:advancedコンパイルしたら動かなかったりなんなりで時間を浪費しました。最終的に完成したコードを見て思ったのは、「JavaScriptで書けばいいんじゃないかと?」、これは自分のClojureScriptのスキルが足りないのが原因な気もします。今回は ClojureScript の勉強だったので良いのですが、この程度のプログラムならそのまま JavaScript で書いたほうが良いのかなと思いました。