ClojureScriptで外部ライブラリを使用する

ものすごい久しぶりにClojureに関するブログを書いてみました。
Clojureをより多くの人に知ってもらうためにも、今後はもっと頻度増やしていこうかなと。

今回は、知っているようで知らない、一度は正しく理解しておきたいClojureScriptでの外部ライブラリの使用方法について紹介します。

ライブラリの使用したい時にまず考えなければいけないのが、それがどんなライブラリであるかということです。大きく分けて3つの分岐があります。

  1. ClojureScriptで書かれたライブラリかどうか
  2. 1以外でGoogle Closure用のライブラリかどうか(goog.provide()使ってるか)
  3. 1と2以外でgoog.provide()を使っていないけど、Google Closureのadvancedコンパイルが通るか

本記事では、上記の分岐を正しく判断できるように、また、ClojureScriptコンパイラに渡せるオプションキーワードである

  • libs
  • foreign-libs
  • externs

そして、ClojureScriptのコード中に現れるexportメタデータについても言及し、
どういうライブラリを使う時に何をすればadvancedコンパイルできるかについて方針を示します。

ClojureScriptで書かれたライブラリを使う

cljsの拡張子がついたファイルでClojureScriptで書かれたライブラリについては、クラスパスにJARファイル、またはcljsファイルを置き、Clojureのようにrequireやuseしてあげるだけでよいです。lein-cljsbuild使ってる人は、project.cljに:source-pathでソースディレクトリを指定していると思うので、そこの配下にcljsファイルを適当なディレクトリ構造で配置。

上記だけなら、Clojure使いなら特に迷いは無いし、問題はありません。
問題は2と3の場合であるが、何故問題になるかというと、残念ながらGoogle Closure (Library、Compiler)はほとんど使われておらず、それ用のライブラリが少ないという現状があります。

Google Closure Compilerを使うと、jsコードの圧縮・最適化・難読化を一手に引き受けてくれるが、特にadvancedコンパイル時は、関数内の変数と引数、グローバルの変数、関数、プロパティの名前をリネームしたり、デッドコード削除で問題が複雑になります。
cljsファイル側から外部ライブラリの変数や関数を参照する際、リネームやデッドコード削除によって参照できなくなってしまって実行時にエラー起きたり、ライブラリ側がadvancedコンパイルに対応してない場合には特別なコンパイラオプションを渡す必要などが生じます。
こういった一つの一つの複雑なステップがユーザをGoogle Closureからユーザを遠ざけているのかもしれないけど、一つ一つ理解をしてClojureScriptを使いこなしたいものですね。

Google Closure用のライブラリを使う

JavaScriptは言語レベルで名前空間管理のための機構は用意してないが、Google Closureでは、goog.provide()とgoog.require()によってそれぞれパッケージ作成とロードを行います。ClojureScriptの名前空間管理ではこの仕組みを利用しています。

例えば以下のような、コンソールに文字列出力するだけのGoogle Closure用ライブラリlib.jsがあったとすると、

goog.provide("hoge.lib");

hoge.lib = function () {};

hoge.lib.log = function() {
    console.log("hoge lib");
};

この場合は、コンパイラ(lein-cljsbuildの場合はproject.clj)にlibsオプションキーワード、値にはベクタとしてjsファイルのパス(project.cljからの相対パス等、複数指定可)を渡してあげればよい。

{:output-to "resources/public/js/core.js"
 :optimizations :advanced
 :libs ["jslib/lib.js"]}

ClojureScriptでライブラリを使いたい場合には、普通にrequireする。

(ns hoge.core
  (:require [hoge.lib :as lib]))

(lib/log)

Google Closureのadvancedコンパイルに対応しているライブラリを使用する

世の中のjsライブラリはGoogle Closure用に作られてないし・・・でもどうしてもadvancedコンパイルをしたい!という至極当然のニーズがあるかと思います。
Web用途にClojureScriptを使う場合、advancedかつpretty-print falseかつgzipなしで80KBとかだったファイルがsimple、whitespaceコンパイルで数百KBとかファイルサイズが増えたりするので。

この時に考えるのが、advancedコンパイル可能なjsファイルであるかどうか。大抵のライブラリは互換性が無いのですが、もし互換性がある場合にはコンパイラオプションにforeign-libsキーワードを使うことでコンパイルできるようなります。

もし、自作のadvanced互換なjsライブラリを作りたい場合にはGoogle公式資料が参考になるかと。
Advanced Compilation and Externs - Closure Tools — Google Developers

foreign-libsを使用する場合には以下のように記述します(ベクタには複数マップ指定可)。
fileにはproject.cljなどからの相対パス、providesには、本来goog.provide()で指定すべきパッケージ名を書きます。

{:output-to "resources/public/js/core.js"
 :optimizations :advanced
 :foreign-libs [{:file "resources/public/foreign_lib.js"
                    :provides ["hoge.foreign_lib"]}]}

あとは、libsの時と同じようにClojureScriptのコード内でパッケージをrequireして使用する。
foreign-libsにより内部的に何が行われるかというと、本来goog.provide()を指定していなかったjsファイルであるが、foreign-libsを指定することでgoog.provide()が挿入され、あとはlibsと同じようにコンパイルされるだけである。
すなわち、goog.provide()を使わないだけの状態で後はGoogle Closure対応ですよという珍しいライブラリのみに使える技である。

当然、世の中のライブラリが何も意識せずに対応していました!なんてことは珍しく、ほとんどのライブラリではそんな都合の良いことなんて無いので、次は最終奥義externsについて説明したい。

advancedコンパイルに対応していないライブラリを使用する場合

foreign-libs使用してadvancedコンパイル通してもうまくいかない。どうしてもあのメジャーな○○ライブラリを使いたいなどあるかと思います。これを知るまでは自分もあきらめていました。

そうです・・・そんな時はexternsを使いましょう!!

advancedコンパイルで起こる問題の要因は何かというと・・・リネームとデッドコード削除。これでしたね。
externsによって何がうれしいかというと、externsで指定されたjsファイル内で参照される変数、プロパティは絶対に削除すんなよ、名前変えんじゃねーよと脅すことで、コンパイラがビビってそれらのリネーム、デッドコード削除をしないということです。

これによる弊害は、externs内に使いたい変数、プロパティをいちいち指定しなければならないなどといった手間が増えますが、どうしても使いたいライブラリがある場合にはやる価値は十分にあるかと思います。
また、開発中はadvancedは基本使わないと思うので、prod用に必要な分だけexterns指定するとかで最小限の手間で抑えることができます。

ちなみに、別の記事で書くつもりですが、externsによってTitanium Mobileで普通にClojureScriptのadvancedコンパイルが通ったりします。これにより、ClojureScriptによるスマホネイティブアプリ(jsインタプリタ使用ですが)の開発が可能に!

では、externsを使用する際の具体例を示したいと思います。Web用途でいうと、よく下記のように使われます。

  • externs用のjsファイルを用意
  • project.cljにてexternsとして上記jsを指定
  • html内でscriptタグでjsライブラリをロード
  • ClojureScriptからそのjsライブラリをjs名前空間によって参照して利用

具体例として、cljsファイル内からexterns指定が必要な外部ライブラリを使ってコンソールに文字列を出力する例を考えたいと思います。
まずはcljsファイルについて。

(ns hoge-extern.core)

(js/fuga.extern.log)

次にexterns用のjsファイル extern.js。

fuga = {};
fuga.extern = {};
fuga.extern.log = {};

変数の中身には適当な値を突っ込んでおけば大丈夫です。

コンパイラオプションとしてexternsを指定します。(ベクタには複数ファイル指定可)

{:output-to "resources/public/js/core.js"
 :optimizations :advanced
 :externs ["jslib/extern.js"]}

以下ライブラリ fuga.js。

fuga = {};
fuga.extern = function() {};
fuga.extern.log = function() {
    console.log("fuga");
};

最後にhtmlファイル index.html。

<html>
  <head>
    <script type="text/javascript" src="/fuga.js"></script>
    <script type="text/javascript" src="/core.js"></script>
  </head>
</html>

外部JSライブラリからadvancedコンパイルしたcljsファイル内の関数を参照したい

これまでClojureScript側からライブラリを使う方法について説明してきましたが、逆にjs->cljsで関数を呼び出したい場合があるかと思います。
当然、advancedコンパイルをかけてしまうと、リネームされて名前が変わってしまう可能性があるので何もしないとおそらく動きません。

そんな時には、ClojureScriptにて関数定義を行うdefnに対してexportメタデータを指定します。
exportを指定することで、その関数名はリネームされないので外部jsライブラリからの呼び出しが可能となり、advancedコンパイルに対応します。

以下具体例として、jsライブラリ側からexport指定されたcljs内の関数を呼び出して文字列を出力する場合を考えます。
リネームによるファイルサイズ圧縮の恩恵を受けたいので、ベストプラクティスとしては、何でもかんでも関数をexportするのではなく、cljs内のエントリポイントとなる関数のみにexport指定するのが良いです。

まず、エントリポイントとなる関数を定義したcljsファイル core.cljs。

(ns hogehoge.core)

(defn ^:export init
  []
  (js/console.log "hogehoge"))

次にhtmlファイル index.html。

<html>
  <head>
    <script type="text/javascript" src="./core.js"></script>
    <script type="text/javascript" src="./export.js"></script>
  </head>
</html>

最後にエントリポイントの関数を呼び出すだけのjsファイル export.js。

hogehoge.core.init();

まとめ

いかがでしたでしょうか。libs、foreign-libs、externs、exportにより、advancedコンパイルという巨大なブラックボックスと戦う武器を手に入れたことだと思います。これでぜひぜひ快適なClojureScriptライフを!

最後に、最近出たClojureScript本であるClojureScript: Up and RunningにClojureScriptからの外部ライブラリ使用について素敵なフローチャートがありましたので、日本語に意訳、多少改変したものを作りましたので貼り付けておきます。
よかったらお使いください。

ClojureでMagicPacket送信ツールを作ってみた

Clojureで書くとこんな感じかな。

(ns
    #^{:author "otabat",
       :doc "Magic packet sender"}
  masquerade
  (:gen-class))

(import '(java.net InetSocketAddress DatagramPacket DatagramSocket))
(use '[clojure.contrib.str-utils :only (re-split)])

(def default-port 2304)
(def buf-length 102)

(defn send-magic-packet
  "Send magic packet to specified host"
  [ip port mac-byte]
  (let [mac-byte-array (into-array (Byte/TYPE) mac-byte)
	sock (DatagramSocket.)
	host-addr (InetSocketAddress. ip port)
	packet (DatagramPacket. mac-byte-array buf-length host-addr)]
    (.setBroadcast sock true)
    (.send sock packet)))

(defn repeat-seq
  [n buf]
  (loop [res '() i n]
    (if (zero? i)
      res
      (recur (concat res buf) (dec i)))))

(defn string-hex-to-byte-digit
  [mac]
  (for [x (re-split #":" mac)] (byte (Integer/parseInt x 16))))

(defn is-port
  "Determine whether it is a valid port number."
  [port]
  (and (>= port 0)
       (<= port 65535)))

(defn is-mac
  "Determine whether it is a valid mac address."
  [mac]
  (let [patn "^((([a-fA-F0-9]){2}):){5}([a-fA-F0-9]){2}$"]
    (and (= mac
	    (first (re-find (re-pattern patn) mac))))))

(defn is-ip
  "Determine whether it is a valid ip address."
  [ip]
  (let [p "(\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])"
	patn (str "^" p "\\." p "\\." p "\\." p "$")]
    (and (= ip
	    (first (re-find (re-pattern patn) ip))))))

(defn -main
  [ip mac port]
  (let [port (Integer/parseInt port)
	buf (repeat 6 (byte 0xff))]
    (when-not
	(and (is-ip ip)
	     (is-mac mac)
	     (is-port port))
      (println "Invalid input.")
      (System/exit 0))
    (let [mac-byte (string-hex-to-byte-digit mac)
	  buf (concat buf (repeat-seq 16 mac-byte))]
      (send-magic-packet ip port buf))))

ついでにビルド方法も。
Leiningen使ってる人は以下のようにやればOK。

$ lein compile
     [null] Compiling masquerade
$ lein uberjar
All :namespaces already compiled.
Created /プロジェクトのパス/masquerade.jar
Including masquerade.jar
Including clojure-1.1.0-master-20091231.150150-10.jar
Including clojure-contrib-1.1.0-master-20100114.180141-21.jar

最後に起動方法。

$ java -jar masquerade-standalone.jar xxx.xxx.xxx.xxx MM:MM:MM:MM:MM:MM 2304