Clojure勉強日記(その8 3.1 Javaの呼び出し
こんにちは。とりあえず構文の説明が終わり、後は実際に機能をコードを書きながら確認していく形になりそうですね。
ClojureコードはJavaバイトコードに直接コンパイルされ、Javaを呼び出す際に変換が挟まれることはない。
Clojureは直接JavaのAPIを活用できる。
普通、ClojureのコードはJavaのAPIを生で実行し、Lisp風にすることはない。
→ これは結構大きい要素ですね。LispっぽいけどやはりJVM言語と思えるのはこの要素があってこそ。
その他のポイント。
- Javaの呼び出しは単純で直接的
- プリミティブ型、配列についてのサポートと型ヒント(リードメタデータで定義)によってClojureのコンパイラにJavaのコンパイラの出力と同等のコードを出力させることができる
- JavaからClojureを呼び出すことも出来る。ClojureはJavaのクラスを動的に生成できる
- Clojureの例外処理は簡単。検査例外のような強制はないし、イディオムによってリソースを解放できる
・・・これを読んでいると、Stormの裏にちょこちょこ手を出したい場合はClojureで書いた方がいい気がしますねぇ。
ともあれ、それは別の話。
1.コンストラクタ、メソッド、フィールドへのアクセス
Clojureではnew特殊形式を用いることでJavaのオブジェクトを生成することが可能。
new classname
defで実際の変数にバインドして使用する形になる。
user=> (def randomObj (new java.util.Random)) #'user/randomObj user=> (class randomObj) java.util.Random user=> (str randomObj) "java.util.Random@6657968b"
インスタンスを生成した後はClojureのドット(.)特殊形式を使うことでメソッドを実行できる。
(. class-or-instance member-symbol & args)
(. class-or-instance (member-symbol & args))
下記のようにメソッドを実行できる。
尚、Javaのメソッドを実行する場合自動的に型ヒントが入っているらしく、異なる型を指定するとClojureからエラーが返る。
user=> (. randomObj nextInt 1) 0 user=> (. randomObj nextInt) -23224175 user=> (. randomObj nextInt 10) 0 user=> (. randomObj nextInt 10) 5 user=> (. randomObj nextInt "10") IllegalArgumentException Unexpected param type, expected: int, given: java.lang. String clojure.lang.Reflector.boxArg (Reflector.java:432)
クラスメソッドについてはインスタンスの代わりにクラスを指定することでアクセス可能。
user=> (. Math PI) 3.141592653589793
Javaのクラスについてはインポート宣言をしておくことでパッケージ名を付けずにアクセス可能となる。
(import [& import-lists])
; import-list => (package-symbol & class-name-symbols)
下記のimport式を使えばパッケージ名を付けずにアクセス可能となる。
・・・となったが、パッケージ配下に複数クラスを指定するとインポート出来ない模様。
仕様が変わったのか、記法誤りなのか・・・はてさて。
user=> (import '(java.util Random Locale)) java.util.Locale user=> Ramdom CompilerException java.lang.RuntimeException: Unable to resolve symbol: Ramdom i n this context, compiling:(NO_SOURCE_PATH:0:0) user=> Locale java.util.Locale user=> (import '(java.util Random)) java.util.Random user=> (import '(java.util Random) '(java.text MessageFormat)) java.text.MessageFormat user=> (import '(java.util Map) '(java.text SimpleDateFormat)) java.text.SimpleDateFormat user=> Map java.util.Map user=> SimpleDateFormat java.text.SimpleDateFormat
これがClojureからJavaを呼び出す際の基本。
これで大抵のことはできるが、ただそれだけとのこと。
Clojureが提供するのはさらにその先らしい。
2.構文糖衣
前述したJavaの式はより短い形式で置き換えることができる。
user=> (new Random) #<Random java.util.Random@433245d6> user=> (Random.) #<Random java.util.Random@2cc2022e> user=> (. Math PI) 3.141592653589793 user=> (Math/PI) 3.141592653589793 user=> (System/load "Test") UnsatisfiedLinkError Expecting an absolute path of the library: Test java.lang. Runtime.load0 (Runtime.java:789) user=> (def rnd (Random.)) #'user/rnd user=> (. rnd nextInt) 1482978952 user=> (.nextInt rnd) 874054458
個人的にはstaticメソッド/フィールドの呼び出しについてはわかりやすいが、
コンストラクタと.method・・・というのは微妙かも。
JavaのAPIを使用していると参照を何段もたどって値を取得しなければいけないことがある。
例えば、あるオブジェクトのソースコードのURLを取得したければ
クラス→プロテクションドメイン→コード→ロケーション・・・というように。
ん。
これだとシュガーシンタックスを使うと『後から呼び出す処理の方が先に記述する』となるため
Java使い的にはわかりにくいですね・・・
; シュガーシンタックス版1 user=> (.getLocation (.getCodeSource (.getProtectionDomain (.getClass #{1 2})))) #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-1.5.1.jar> ; 通常版 user=> (. (. (. (. #{1 2} getClass) getProtectionDomain) getCodeSource) getLocation) #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-1.5.1.jar>
上記のような式に対しては更にシュガーシンタックスが存在する。
(.. class-or-instance form & forms)
..は複数のメンバへのアクセス式をそれぞれのステップの結果をthisオブジェクトとしながらつないでいく。
上記のコードは下記のように記述できる。
; シュガーシンタックス版1 user=> (.. #{2 3} getClass) clojure.lang.PersistentHashSet user=> (.. #{2 3} getClass getProtectionDomain) #<ProtectionDomain ProtectionDomain (file:/C:/AcroWorks/GitHub/clojurestudy/lib/clojure-1.5.1.jar <no signer certificates>) sun.misc.Launcher$AppClassLoader@21e30857 <no principals> java.security.Permissions@74b01999 ( ("java.lang.RuntimePermission" "exitVM") ("java.io.FilePermission" "\C:\AcroWorks\GitHub\clojurestudy\lib\clojure-1.5.1.jar" "read") ) > user=> (.. #{2 3} getClass getProtectionDomain getCodeSource) #<CodeSource (file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-1.5.1.jar <no signer certificates>)> user=> (.. #{2 3} getClass getProtectionDomain getCodeSource getLocation) #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-1.5.1.jar>
これだと確かにJava使いにはわかりやすい。メソッドを実行した結果を取得して更に実行・・・という用途にはいい。
ただ、Lispとしては「内側→外側」と呼んでいくのが常なのにも関わらず、Java的に「左側→右側」と読まれる・・
と考えると、わかりやすい理由がわかる。
..マクロではなく、dotoマクロは「同じインスタンスに対してメソッドを順次実行する」を書くことが出来る。
(doto class-or-instance & member-access-forms)
doが含まれていることからわかるように、dotoはオブジェクトの変更可能なJava世界での副作用を起こすのに使用できる。
というかこれもおそらく最後の実行結果しか返り値に影響を与えない関係上副作用を起こすしか用途はない。
尚、これについては並列実行ということはない…模様。規模小さすぎるのはあるが。
user=> (doto (new java.util.HashMap) (.put "Test" "Test")) {"Test" "Test"} user=> (doto (new java.util.HashMap) (.put "Test" "Test") (.put "Test" "Test2")) {"Test" "Test2"} user=> (doto (new java.util.HashMap) (.put "Test" "Test") (.put "Test1" "Test1") (.put "Test" "Test2") ) {"Test" "Test2", "Test1" "Test1"}
3.Javaのコレクションを使う
Clojureのコレクションはほとんどの用途でJavaのコレクションを置き換えることが出来る模様。
・・・そのため、Clojureを一部Javaに含めて使用するとコレクションが軒並みClojureのものに置き換わり、
Java側で使用する際に非常に大変になる事態が発生し得るわけではあるが。
ただ、下記のような特徴もあるため、使った方がいいのは確かなのだろう。
- スレッドセーフ
- 性能はいい
- Javaのインタフェースを実装している
・・・あれ。もしかすると以前あれだけ苦労したのは適当なキャストを行ったから(汗)?
尚、Javaの配列は特殊構文を保持しており、バイトコードも異なる。
そのためJavaのコレクションも、Clojureのコレクションも配列のふりをすることはできない。
結果、Javaで配列を使用する場合にはClojure側でもそれに合わせた実装が必要になる。
Clojureのmake-arrayでJava配列を作成することが出来る。
(make-array class length)
(make-array class dim & more-dims)
Clojureのseqを使用すると配列をシーケンスとして使用することが出来る。
user=> (make-array String 5) #<String[] [Ljava.lang.String;@24158c28> user=> (seq (make-array String 5)) (nil nil nil nil nil) user=> (str (seq (make-array String 5))) "(nil nil nil nil nil)" user=> (class (seq (make-array String 5))) clojure.lang.ArraySeq
尚、Clojureはプリミティブ型の配列を作成するために各々別個の関数も定義している。
user=> (find-doc "-array") ------------------------- clojure.core/boolean-array clojure.core/byte-array clojure.core/char-array clojure.core/double-array clojure.core/float-array clojure.core/int-array clojure.core/long-array clojure.core/object-array clojure.core/short-array
また、aset、aget、alengthといった配列への低レベルアクセスも提供している。
user=> (def inta (int-array 10)) #'user/inta user=> (aset inta 1 1) 1 user=> (seq inta) (0 1 0 0 0 0 0 0 0 0) user=> (aget inta 1) 1 user=> (aget inta 2) 0 user=> (alength inta) 10
また、下記のようなarray操作用の関数もある。
user=> (find-doc "-array") ------------------------- clojure.core/into-array ([aseq] [type aseq]) Returns an array with components set to the values in aseq. The array's component type is type if provided, or the type of the first value in aseq if present, or Object. All values in aseq must be compatible with the component type. Class objects for the primitive types can be obtained using, e.g., Integer/TYPE. ------------------------- clojure.core/to-array ([coll]) Returns an array of Objects containing the contents of coll, which can be any Collection. Maps to java.util.Collection.toArray(). ------------------------- clojure.core/to-array-2d ([coll]) Returns a (potentially-ragged) 2-dimensional array of Objects containing the contents of coll, which can be any Collection of any Collection.
into-arrayはクラス型を指定して配列を作成する。
自動推測もしてくれる。。。が、なぜかIntegerについては作れない。デフォルトLongで数は扱われるかららしい。
ということは小さい値でいい場合は明示的にintを指定する必要がある模様。
user=> (into-array ["1" , "2", "3"]) #<String[] [Ljava.lang.String;@4a87bd1c> user=> (into-array [1, 2, 3]) #<Long[] [Ljava.lang.Long;@a7ab0f7> user=> (into-array Long [1, 2, 3]) #<Long[] [Ljava.lang.Long;@7aaae6f> user=> (into-array Integer [1, 2, 3]) IllegalArgumentException array element type mismatch java.lang.reflect.Array.set (Array.java:-2) user=> (class 1) java.lang.Long
後はto-array-2dのように2次元配列を作成する関数もある。
user=> (to-array [1, 2, 3]) #<Object[] [Ljava.lang.Object;@1afecda0> user=> (to-array-2d [1, 2, 3]) RuntimeException Unable to convert: class java.lang.Long to Object[] clojure.lang.Util.runtimeException (Util.java:219) user=> (to-array-2d [[1,2] , [2,5] ,[1,2,3]]) #<Object[][] [[Ljava.lang.Object;@14cc077d>
後はClojureのシーケンスに変換せずにJavaの配列全てに触りたい場合は下記のamapを使用する。
(amap a idx ret expr)
amapは配列aのコピーを作り、それをretで指定される名前にバインドする。
その後aの各要素について1回ずつidxを要素のインデックスにバインドしつつexprを評価する・・・って、ややこしい。
とりあえず実際の動きを確認してみる。
user=> (def strings (into-array ["Test1" "Test2" "Test3"])) #'user/strings user=> (seq (amap strings index _ (.toUpperCase (aget strings index)))) ("TEST1" "TEST2" "TEST3") ; 配列として別インスタンスを作成していることの確認 user=> (seq (amap strings index ret (do (println strings) (println ret) (.toUpperCase (aget strings index))))) #<String[] [Ljava.lang.String;@3c3d8fcd> #<String[] [Ljava.lang.String;@4772dd46> #<String[] [Ljava.lang.String;@3c3d8fcd> #<String[] [Ljava.lang.String;@4772dd46> #<String[] [Ljava.lang.String;@3c3d8fcd> #<String[] [Ljava.lang.String;@4772dd46> ("TEST1" "TEST2" "TEST3")
amapに似たareduceというものもある。
(areduce a idx ret init expr)
amapは新たな配列を作成するが、areduceではどんなものでも作り出すことができる・・・とある。
retはまずinitの値にバインドされ、それからexprが呼び出されるたびにその結果に再バインドされる。
これを使うとretの値を用いて保存されている文字列の中から最大のものを抽出したり、
または文字列長を合計したり。。。ということができる。
user=> (areduce strings index ret 0 (max ret (. (aget strings index) length))) 5 user=> (areduce strings index ret 0 (+ ret (. (aget strings index) length))) 15
4.便利な関数
Clojureでは高階関数が定義されており、下記のように「関数を渡して」処理を行うことができる。
user=> (map (fn [n] (.toUpperCase n)) strings) ("TEST1" "TEST2" "TEST3")
だが、「.toUpperCase」自体はJavaのメソッドであり、Clojureの関数では無い。
そのため、直接高階関数に適用すると下記のようにエラーとなる。
user=> (map .toUpperCase strings) CompilerException java.lang.RuntimeException: Unable to resolve symbol: .toUpper Case in this context, compiling:(NO_SOURCE_PATH:16:1)
Clojureにはメソッドをラップしてくれるmemfnというマクロが用意されている。
これを用いるとメソッドをラッピングしてメソッドとすることができる。
user=> (map (memfn toUpperCase) strings)
後はあるインスタンスが特定のクラスかどうかを調べるinstance?関数もある。
user=> (instance? Integer 10) false user=> (instance? Long 10) true user=> (instance? Number 10) true
JavaのformatメソッドはClojureでは引数の渡し方が面倒となるため、Clojure側ではラッパー関数を用意している。
(format fmt-string & args)
user=> (format "{0} TestFormat {1}" "This is" 5) "{0} TestFormat {1}" user=> (format "%s TestFormat {1}" "This is" 5) "This is TestFormat {1}" user=> (format "%s %s TestFormat %d" "This is" 5) MissingFormatArgumentException Format specifier 'd' java.util.Formatter.format (Formatter.java:2487) user=> (format "%s %s TestFormat" "This is" 5) "This is 5 TestFormat" user=> (format "%s %d TestFormat" "This is" 5) "This is 5 TestFormat"
残念ながらMessageFormatはそのまま使えないらしい。
既存のJavaEntityからデータを読むときにClojureマップに変換しておくと便利なケースがある。
Clojureのbean関数はJavaBeanを変更不可なClojureのマップに変換する。
user=> (bean 5) {:class java.lang.Long} user=> (bean "String") {:empty false, :class java.lang.String, :bytes #<byte[] [B@e27fdb9>} user=> (:bytes (bean "String")) #<byte[] [B@459371b9>
なお、フィールド名はKeywordクラスとなって使用されるらしい。
今回の例の場合はあまり意味がないが、実際のEntityの場合map化することで
Clojureのマップ関数が使用できるようになる。
#例えば、特定のキーワードを関数にとって特定のフィールドを取得する・・・など