『プログラミングClojure 第2版』にJava API呼び出し用の構文糖衣の項が無い? なのでメモ

積読のまま第2版を買ったので、現在手元にそれぞれ1冊ずつある状況なのだけど……。

プログラミングClojure

プログラミングClojure

プログラミングClojure 第2版

プログラミングClojure 第2版

本棚に空きがないので、第2版を残しておいて第1版の方を処分しようかと思ったら、どうも第1版にあったJava API呼び出しの構文糖衣の説明が第2版に見当たらない。うーん、探し方が悪いだけなのだろうか?

とりあえずメモを残しておく。他にも無くなった項目があるかもしれないけど、そもそも第1版をそこまで読んでいない(何しろ積読だから……)ので問題ない。

パッケージのインポート

一応、パッケージ名付きでも大丈夫。

(new java.util.Random)

しかし毎度パッケージ名付きで書くのは面倒なので、importを使う。

(import java.util.Random)
(new Random)

同一パッケージの複数のシンボルをインポートする場合は、リストで書く。

(import '(javax.xml.transform.stream StreamResult StreamSource))

同一パッケージ内の全てのシンボルをインポートする方法は不明。

一度に色々とインポートすることも可能。

(import java.io.File
        javax.xml.transform.TransformerFactory
        '(javax.xml.transform.stream StreamResult StreamSource))

特定のネームスペースにインポートしたい場合はnsの:importで。

(ns foo.bar
  (:import java.io.File
           javax.xml.transform.TransformerFactory
           [javax.xml.transform.stream StreamResult StreamSource]))

Clojureのドキュメントによると、nsの:importでインポートしたシンボルは、普通にimportでインポートしたシンボルものより優先されるらしい。

Javaのオブジェクトの生成

基本はnew特殊形式。

; (import java.io.File
;         java.util.Random)
(new Random)
(new File "foo.txt")

構文糖衣で Classname. という形式。

(Random.)
(File. "foo.txt")

メソッドやフィールドへのアクセス

基本はドット(.)特殊形式。

(def rnd (Random.))
(. rnd nextInt 10)
(. Math PI)
(. System exit 0)

インスタンスのメソッド/フィールドだけでなく、クラスのメソッド/フィールド(staticなメソッド/フィールド)でも同じ。

しかし構文糖衣は、インスタンスのメソッド/フィールドとクラスのメソッド/フィールドとで異なる。

(.nextInt rnd 10)
Math/PI
(System/exit 0)

インスタンスのメソッド/フィールドでは .methodOrFieldName という形式を使い、クラスのメソッド/フィールドでは Classname/membername という形式を使う。クラスのフィールドでは括弧は不要だが、クラスのメソッドでは必要なことに注意。

.methodOrFieldName の形式は、JavaC++などに慣れた身には違和感があるけど、よく考えればメソッド名がリストの先頭に来るあたりはLisp的な気がする。よく知らないけど、CLOSでも「(method instance)」という順に記述するみたいだ。Classname/membername は――「/」も含めて一つのシンボルとして扱っている感じで、これもLisp的といえばそうなのかも。

間接参照(メソッドチェーン的なもの)

.methodOrFieldName の形式は、間接参照が多段になるとコードが読みにくくなる。例えば『プログラミングClojure』のこのコード。

(.getLocation (.getCodeSource (.getProtectionDomain (.getClass '(1 2)))))

Javaだとこんな感じだろうか?

// list == '(1 2)
list.getClass().getProtectionDomain().getCodeSource().getLocation();

この場合はJavaのコードの方が読みやすいだろう。

ドット(.)特殊形式を使うとJavaのコードに近くなる。

(. (. (. (. '(1 2) getClass) getProtectionDomain) getCodeSource) getLocation)

しかしこれはこれで面倒だ。

そこで構文糖衣として .. マクロが用意されている。

(.. '(1 2) getClass getProtectionDomain getCodeSource getLocation)

随分とスッキリした。

.. はマクロなので、macroexpandやmacroexpand-1で展開した結果を見ることができる。

(macroexpand '(.. '(1 2) getClass getProtectionDomain getCodeSource getLocation))
; => (. (. (. (. (quote (1 2)) getClass) getProtectionDomain) getCodeSource) getLocation)

ドット(.)特殊形式を使ったコードに展開していることが分かる。

構文糖衣の合わせ技

クラスのメソッド/フィールドとインスタンスのメソッド/フィールドが混在したコードを構文糖衣を使って書くと、例えばこんな感じ。

(.println System/err "foo bar baz")
(. System/err println "foo bar baz")

「err」は「System」のstaticなフィールドなので Classname/membername を使い、printlnは「err」の型であるクラス PrintStream のインスタンスメソッドなので .methodOrFieldName の形式ないしドット(.)特殊形式ないしを使う。

ところでドット(.)特殊形式はクラスのメソッド/フィールドとインスタンスのメソッド/フィールドの区別無く使用できるだった。

(. (. System err) println "foo bar baz")

なのでドット(.)特殊形式に展開する .. マクロを使用できる。

(.. System err (println "foo bar baz"))

『プログラミングClojure』(第1版のほう)では、Javaの呼び出しにて積極的に構文糖衣を使うべき、と書いてある。ただ、上記のようにクラスのメソッド/フィールドとインスタンスのメソッド/フィールドが混在する時にどの記法をつかうべきか書かれていない。どのパターンを使えばよいのか判断に悩むところだ。

個人的な書き方

とりあえず、以下の書き方で統一しようと思う。

; インスタンスの生成
(Classname.)

; インスタンスのフィールド/メソッドの呼び出し
(.methodOrFieldName instance)
(.methodOrFieldName instance argument)

; クラスのフィールド/メソッドの呼び出し
Classname/fieldname
(Classname/method)
(Classname/method argument)

; インスタンスのメソッドのチェーン
(.. instance method-1 (method-2 arg) method-3)

; クラスのフィールド + インスタンスのフィールド/メソッド
; クラスのフィールドを 1つのシンボルと見なす
(.methodOrFieldName Classname/fieldname)
(.methodOrFieldName Classname/fieldname argument)

; 仮にクラスのフィールドの後にインスタンスのメソッドのチェーンがある場合
(.. Classname/fieldname method-1 method-2)
(.. Classname/fieldname method-1 (method-2 arg) method-3)

; クラスのメソッド + インスタンスのメソッド
; クラスのメソッドを 1つのシンボルと見なすのは(括弧がつくので)苦しい……
(.. Classname method-1 method-2)
(.. Classname method-1 method-2 method-3)
(.. Classname (method-1 argument) method-2 method-3)
(.. Classname method-1 (method-2 argument) method-3)