ClojureでJavaクラスのコンストラクタをapplyする

複数引数のコンストラクタにシーケンスをわたす。

最初こんなコードを書いてエラーになった。

(apply java.util.GregorianCalendar. [2010 5 20])
 ;;=> java.lang.ClassNotFoundException: java.util.GregorianCalendar...

「new class名」の構文糖「class名.」は、applyに渡せないらしい。

(apply new java.util.GregorianCalendar [2010 5 20])
 ;;=> java.lang.Exception: Unable to resolve symbol: new in this context ...

これも駄目。new は関数じゃないから apply できない。

ラムダ式で包めばいい?

(apply #(java.util.GregorianCalendar. %1 %2 %3) [2010 5 20])
 ;;=> OK

これは上手くいった。引数の数が決っている場合これでよさそう。
例えばフォーマットされた文字列からカレンダーオブジェクトを生成する場合、こんな風に書ける。

(use '[clojure.contrib.str-utils :only (re-split)])	

(->> "2010-05-20 10:32:44"
     (re-split #"\D+")
     (map #(Integer/parseInt %))
     (apply #(java.util.GregorianCalendar. %1 %2 %3 %4 %5 %6)))
 ;;=> OK

でも、最後の % が並んでるところがちょっとかっこ悪いなー。

マクロで簡略化できないか?

クラスとシーケンスをとって、クラスのインスタンスを作るマクロ apply-new を作れないか?

(defmacro apply-new [cls & coll]
  (concat `(new ~cls) (first coll)))

(apply-new java.util.GregorianCalendar [2010 5 20])
 ;;=>OK

apply不要で、コンストラクタへシーケンスを渡すことができた。が、次の場合はうまくいかない。

(apply-new java.util.GregorianCalendar (take 3 [2010 5 20 1 1 1]))
 ;;=>java.lang.ClassCastException: clojure.core$take__5095 cannot be cast to java.lang.Number (NO_SOURCE_FILE:0)

マクロ展開してみれば理由は簡単

(macroexpand-1 '(apply-new java.util.GregorianCalendar (take 3 [2010 5 20 1 1 1])))
 ;;=> (new java.util.GregorianCalendar take 3 [2010 5 20 1 1 1])

シーケンス展開によって (take 3 [2010 5 20 1 1 1]) というリストがばらされてしまってる。
つまり apply-new は第二引数が「単なるデータとしてのシーケンス」なのか「関数や特殊形式など評価後の値を得るためのS式」なのかを判断してから、展開を行う必要がある。
どうすればいいのだろう?

最終的には ->> 式の中で apply-new を使いたいのだが、それには上記の問題を解決する必要がる。

解決方法書いてもらった

解決方法 -> ClojureでJavaクラスのコンストラクタをapplyする
まず私のマクロへの突込みから。

(defmacro apply-new [cls & coll]
  (concat `(new ~cls) (first coll)))

こういうのを書くなら

(defmacro apply-new [cls coll]
  `(new ~cls ~@coll))

とするのが楽だと思います。

ごもっともです。いろいろやってるうちにへんに複雑になってた。

こんな感じにすると良いと思います。

; 関数化するマクロ
(defmacro new-fn [class num]
  (let [args (take num (repeatedly gensym))]
    `(fn [~@args] (new ~class ~@args))))

; 普通の関数っぽくやる場合
(apply (new-fn java.util.GregorianCalendar 3) [2010 5 25])
(->> [2010 5 25] (new-fn java.util.GregorianCalendar 3))

最後の実行例は new-fn の前に apply が必要かな。
引数の数を渡すというアイデアは実はもらっていてそういうマクロも試してみました。でもここまでスッキリとは書けなかったな。
確か map関数 を使って %1 %2 %3 ... っていうシンボルを作るようなやり方だったような。
repeatedly ってのがあるの初めて知りました。無限に関数を実行してくれるようです。

; 元エントリっぽくやる場合
(defmacro apply-new [class num args]
  `(apply (new-fn ~class ~num) ~args))

(apply-new java.util.GregorianCalendar 3 [2010 5 25])
(->> [2010 5 25] (apply-new java.util.GregorianCalendar 3))

これを解決する(引数の数を知る)には、引数を展開しなければいけない。
しかし、これはマクロなのでその評価はコンパイル時に行われてしまう。
それが望みの動作とは思えないので、引数の数は渡す必要がある。
第二引数が何者か判断しても、結局同じ問題が起こる。

やっぱりそうか。薄々そんな気はしてました。マクロはコンパイル時解決なので、実行時評価の結果を使うのは無理。考えてみればあたりまえですよね。

一見できた風の代物

(defmacro apply-new-evil [class args]
  `(let [args# (eval '~args)]
     (eval `(new ~~class ~@args#))))

(->> [2005 10 20] (apply-new-evil java.util.GregorianCalendar))

おお、これはすごい。args を eval するとこまでは考えたけど、組み立てたコンストラクタ式をさらに eval か。
macroexpand してみよう

(macroexpand '(->> [2005 10 20] (apply-new-evil java.util.GregorianCalendar)))

(let*
 [args__210__auto__ (clojure.core/eval '[2005 10 20])]
 (clojure.core/eval
  (clojure.core/seq
   (clojure.core/concat
    (clojure.core/list 'new)
    (clojure.core/list java.util.GregorianCalendar)
    args__210__auto__))))

concat で '(new), '(java.util.GregorianCalendar), args__210__auto__ の三つからコンストラクタ式を組み立てて、evalされています。
concat で連結したあと、seq でシーケンスに変換されてるけどこれ必要なのだろうか?
あと関係ないけど、let はマクロだったのか。let* に置き換えられています。確かにClojureの let は機能的に let* だけど内部で let* になっていたとは想像してなかった。

ところでマクロのなかで使われてる ~~class ってなんだろう?

(defmacro foo1 [x] `(`(~x)))
(macroexpand-1 '(foo1 a))

((clojure.core/seq
   (clojure.core/concat
     (clojure.core/list user/x))))

(defmacro foo2 [x] `(`(~~x)))
(macroexpand-1 '(foo2 a))

((clojure.core/seq
  (clojure.core/concat
   (clojure.core/list a))))

構文クォートがネストした場合、引数の値を参照するにはネストの数だけ ~ を書くのか。
そうしないと foo1 のように仮引数 x は仮引数名 x のまま展開されてしまう。
なるほど。

ただし、eval してるのでローカル変数の解決ができない。

 (let [args ["neko"]] (apply-new-evil String args))
; java.lang.Exception: Unable to resolve symbol: args in this context (NO_SOURCE_FILE:164)

結局、役に立たないっぽい。

ここがちょっと私にはわからないな。eval ってレキシカルスコープの名前参照できないんだっけ?
マクロ内でevalすることによって何か問題が起きるのだろうか?
確かに書かれているコードは実行するとエラーになる。

別の方法

ライブラリ探索していて別の方法が見つかりました(こちらの記事を参照)