Advanced Compilation との戦い


今月の Lisp Meet Up で話そうと思っていたんだけど、タイミングがどうも合わないぽいので書くことにした。

まえがき
--------

ClojureScript は Clojure の JavaScript 版です。一般的に Clojure が書ければおおよそ問題なく書けるようになっています。 まあ、色々と魅力はあるものも今回はそれを語るのが目的ではないので割愛。

今回は素敵な ClojureScript の最も厄介な部分といっても過言ではない、 Advanced Compilation について書きたいと思います。

Advanced Compilation とは何か
-----------------------------

まず、話を始める前に ClojureScript が **Google CloSure Compiler** を利用していることを知っておく必要があります。

ClojureScript は Google Closure Compiler が解釈できるような JavaScript を吐きだし、それをさらに Google Closure Compiler がコンパイルします。

その際に、最適化レベルを任意で選択可能です。

-   [Closure Compiler Compilation Levels](https://developers.google.com/closure/compiler/docs/compilation_levels)

最適化のレベルは `WHITESPACE_ONLY`, `SIMPLE_OPTIMIZATIONS`, `ADVANCED_OPTIMIZATIONS` の 3 段階あり(最適化しない、という選択肢を含めれば 4 段階かな)、このうち最後の `ADVANCED_OPTIMIZATIONS` を利用することを Advanced Compilation と呼んでいます(日本語で話しているときはアドバンスドコンパイルとか言うことが多い…?)。

ClojureScript のコンパイラオプションとしては `:optimizations` というキーがあり、これに `:none`, `:whitespace`, `:simple`, `:advanced` のいずれかが設定できるという状態です。

-   [Compiler Options](https://github.com/clojure/clojurescript/wiki/Compiler-Options#optimizations)

実際、 ClojureScript 側でもリリースする場合などは `:advanced` を設定してください(オススメします)というようなことを書いているのですが、これは単純に設定すれば OK という話でもないので困っちゃうというのが今回の話の中心になります。

Advanced Compilation が具体的に何をするかというと、積極的なリネーム、デッドコードの削除、グローバルなインライン化です。デッドコードの削除やグローバルなインライン化によって起き得る弊害というのはそう多くはないでしょう(勿論 `eval` などで呼び出してたら困っちゃいますけど)。

多くの場合は積極的なリネームというのが問題の種になっています。ので、主にこれから書いていく話はそこが中心になります。

積極的なリネームとの戦い方
--------------------------

まず、このリネームの問題が起こるのは ClojureScript のライブラリでも Google Closure Library でもない外部の JS ライブラリを使って interop を利用したケースです。

一般的なこの問題の避け方は次のようになります。

-   外部ライブラリの利用を避ける
-   [CLJSJS](http://cljsjs.github.io/) などで公開されているものを利用する
    -   あるいは自分で作成して公開して利用する
-   extern ファイルをコンパイラに提供する

簡単ですね。これらは公式の Wiki にも記述されている最も一般的な問題の解決方法です。

-   [Advanced Compilation](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation)

とはいえ、外部ライブラリの利用を避けようと思っても jQuery のライブラリなどを利用した方が効率がよい場合などはありますし、 CLJSJS で公開されていない程度にマイナーで自分で作成するにはコストの方が高すぎるし、 extern ファイルを自分で書くにも深く馴染みがあるわけでなければ結構難しいと思います。

自力で externs ファイルを書く道を選んだ方は次の記事が非常に良くまとまっていて有用なので参考にすると良いと思います。

-   [ClojureScriptで外部ライブラリを使用する](http://otabat.hatenablog.com/entry/20121123/1353674855)

今回、出来るだけ労をかけずに外部ライブラリを利用したかったので、自力で extern ファイルを用意することは避けたかったです。なので、 extern ファイルを書かずに戦う方法の紹介をします。

### QA ビルドを用意する

開発時には絶対分からない問題なので、 Dev, Release 以外にも QA ビルドを用意してテスト環境にデプロイするものはそちらを利用するなどという処置が適切です。 つまり、開発環境では最適化レベルを `:none` 、 Release では `:advanced` とするわけですが、 QA は最適化レベルを `:advanced` としつつも `:pseudo-names true` と `:pretty-print true` とオプションを設定することでテスト環境へデプロイした際に問題を発見/解決しやすくなるでしょう。

-   [Fixing Advanced Compilation Issues](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation#fixing-advanced-compilation-issues)
-   [Compiler Options](https://github.com/clojure/clojurescript/wiki/Compiler-Options)

### 既にある extern ファイルを流用する

このリネーム問題は Google Closure Compiler が起因の問題なので、 Google Closure Compiler ユーザーが既に通った道のはずです。つまり、それなりにメジャーなライブラリなどであれば extern ファイルがある可能性があります。

-   [closure-compiler](https://github.com/google/closure-compiler)
-   [node.js-closure-compiler-externs](https://github.com/dcodeIO/node.js-closure-compiler-externs)

Google Closure Compiler のリポジトリには `/contrib/externs` 下に幾つかのメジャーライブラリの extern ファイルが存在するのでそこから拝借すると良いでしょう。 (余談ですが、 jQuery の 1.9 以降がないのは 1.9 以降で大きな変更がなかったからのようです。 See also: [Externs for jQuery 2.x](https://github.com/google/closure-compiler/issues/970) )

### fence を利用する

-   [fence](https://github.com/myguidingstar/fence)

上記の 2 点は多くの人が辿りつくところだと思いますが、今回一番紹介したかったのはこれです。 fence というまんまな名前のライブラリなんですが extern ファイルを一行も書くことなく(あるいは用意することなく) ClojureScript を気軽に Advanced Compilation できるという代物です。

例えば次のように jQuery を利用しているコードを考えます。

``` clojure
(ns demo.core)

(let [elm (js/$ "#your-input")]
  (.keydown elm
            (fn []
              (.text (js/$ "#copied")
                     (.val elm)))))
```

これはいわゆる「開発環境だと動くけど、リリースすると動かなくなる」コードです。普通であれば extern ファイルを用意するか、どうにかして頑張るところですがこれを次のようにします。

まずは \`project.clj\` に \`\[fence "0.2.0"\]\` を依存しているライブラリとして記述して対象のコードを \`fence.core/+++\` マクロで囲みます。

``` clojure
(ns demo.core
  (:require-macros [fence.core :refer [+++]]))

(let [elm (js/$ "#your-input")]
  (+++
   (.keydown elm
             (fn []
               (.text (js/$ "#copied")
                      (.val elm))))))
```

これだけで Advanced Compilation に耐えることが出来るようになりました。

ただ、これで安泰かというとそうでもなかったりします。マクロ展開などのタイミングのせいだと思うんですけど、例えばスレッディングマクロで書いているところはどうしようもなかったりします。

``` clojure
(-> (js/$ "ul")
    (.find "li:eq(2)")
    (.css "color" "#F00")
    (.end)
    (.css "border" "1px solid #000"))
```

このコードを次のように fence のマクロで囲っても実行時にこけます。

``` clojure
(+++
 (-> (js/$ "ul")
     (.find "li:eq(2)")
     (.css "color" "#F00")
     (.end)
     (.css "border" "1px solid #000")))
```

なので、次のように解決したりします(そもそも jQuery は extern ファイルが容易に手に入るのでこのようなことするまでもないですが)。

``` clojure
(defn jq-find [this arg]
  (+++ (.find this arg)))

(defn jq-css [this arg1 arg2]
  (+++ (.css this arg1 arg2)))

(defn jq-end [this]
  (+++ (.end this)))

(-> (js/$ "ul")
    (jq-find "li:eq(2)")
    (jq-css "color" "#F00")
    (jq-end)
    (jq-css "border" "1px solid #000"))
```

わりと力技な気がするけど気のせいです。このように有用ではあるのですが、万能ではないのでたまにうまくいかないかもしれないというところだけ気にかけておくと良いでしょう。

### Leiningen の extern ファイルを自動生成する系のプラグイン

ふたつくらいあるような気がしますけど、ビルドプロセスに組込むのがめんどくさいので個人的に利用していないです(あと精度がいまいちよく分からない)。

まとめ
------

Advanced Compilation でハマることはちょこちょこあるのですが、解決方法さえ確立させてしまえばたいした問題ではありません。
ちなみに最適化レベル `:simple` でコンパイルした弊社のアプリは 2MB にまでなりましたが、 `:advanced` にすることで 500KB まで落すことができたのでしっかり Advanced Compilation していきましょう。

参考
----

-   [Closure Compiler Compilation Levels](https://developers.google.com/closure/compiler/docs/compilation_levels)
-   [Advanced Compilation and Externs](https://developers.google.com/closure/compiler/docs/api-tutorial3)
-   [Understanding the Restrictions Imposed by the Closure Compiler](https://developers.google.com/closure/compiler/docs/limitations)
-   [Closure Compiler Service](http://closure-compiler.appspot.com/home)
-   [Compiling Clojure to Javascript](http://blog.fogus.me/2011/07/21/compiling-clojure-to-javascript-pt1/)
-   [Advanced Compilation](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation)
-   [Compiler Options](https://github.com/clojure/clojurescript/wiki/Compiler-Options)
-   [fence](https://github.com/myguidingstar/fence)