読者です 読者をやめる 読者になる 読者になる

夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その8 3.1 Javaの呼び出し

こんにちは。とりあえず構文の説明が終わり、後は実際に機能をコードを書きながら確認していく形になりそうですね。

ClojureコードはJavaバイトコードに直接コンパイルされ、Javaを呼び出す際に変換が挟まれることはない。
Clojureは直接JavaAPIを活用できる。
普通、ClojureのコードはJavaAPIを生で実行し、Lisp風にすることはない。
 → これは結構大きい要素ですね。LispっぽいけどやはりJVM言語と思えるのはこの要素があってこそ。

その他のポイント。

  • Javaの呼び出しは単純で直接的
  • プリミティブ型、配列についてのサポートと型ヒント(リードメタデータで定義)によってClojureコンパイラJavaコンパイラの出力と同等のコードを出力させることができる
  • JavaからClojureを呼び出すことも出来る。ClojureJavaのクラスを動的に生成できる
  • 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・・・というのは微妙かも。

JavaAPIを使用していると参照を何段もたどって値を取得しなければいけないことがある。
例えば、あるオブジェクトのソースコードの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側で使用する際に非常に大変になる事態が発生し得るわけではあるが。
ただ、下記のような特徴もあるため、使った方がいいのは確かなのだろう。

  1. スレッドセーフ
  2. 性能はいい
  3. 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のマップ関数が使用できるようになる。
#例えば、特定のキーワードを関数にとって特定のフィールドを取得する・・・など