夢とガラクタの集積場

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

Clojure勉強日記(その10 3.3 ClojureでJavaのクラスを作る(Vol_1)

ClojureのオブジェクトはJavaに対応するものがあればそのインタフェースを実装している。

  1. Clojureのデータ構造はJavaのコレクションAPIを実装している
  2. Clojureの関数はRunnableとCallableを実装している

・・・後者は中々。このあたりやはり並列実行用の言語と感じる。
その他のJavaプロキシを作成する方法は下記。

1.Javaプロキシを作る

Javaとやり取りをする場合にJavaのインタフェースを実装しなければならない場合がある。
SAXパーサをClojureで書く場合や、StormのClojure型のデータ部分を解析する場合など。
・・・とはいえ、後者は難しいのでもちっと後に。

SAXパーサを記述する際にはJavaクラスを拡張して
特定の要素を見つけた場合のコールバック関数を定義する必要がある。
その場合、Clojureではproxy関数を用いてJavaクラスをextend出来る。
(proxy class-and-interface super-constract-args & functions)

まずは簡単な例として、SAXパーサを考える。
SAXパーサをDefaultHandlerクラスを継承して作成する。
実際に書いてみると下記のようになる。

user=> (import '(org.xml.sax InputSource) '(org.xml.sax.helpers DefaultHandler)'(java.io StringReader) '(javax.xml.parsers SAXParserFactory))
user=> (def print-element-handler (proxy [DefaultHandler] [] (startElement [uri local qname attrs] (println (format "Find element: %s" qname)))))
#'user/print-element-handler

後は実際にこの継承クラスを用いてパースを行う関数を作成して実行となる。

user=> (defn demo-sax-parse [source handler] ((.. SAXParserFactory newInstance newSAXParser) parse (new InputSource (new StringReader source)) handler)))
user=> (demo-sax-parse "<foo><bar>BodyBar</bar></foo>" print-element-handler)
Find element: foo
Find element: bar
nil
user=> (demo-sax-parse "<foo><bar>BodyBar</bar></foo" print-element-handler)
Find element: foo
Find element: bar
SAXParseException XMLドキュメント構造は、同じエンティティ内で開始および終了する必要があります。
com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException (ErrorHandlerWrapper.java:198)

微妙にここまで深くなるとわかりにくいため、一度状況を確認しながら実行する。

user=> (class (.. SAXParserFactory newInstance newSAXParser))
com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl
; メソッド呼び出し構成がわかりやすいよう構成変更
; こうすることで、SAXParserImplのメソッドparseをInputStreamとhandlerで利用していることが明確になる
(defn demo-sax-parse [source handler] (.parse (.. SAXParserFactory newInstance newSAXParser)  (new InputSource (new StringReader source)) handler))
#'user/demo-sax-parse
user=> (demo-sax-parse "<foo><bar>BodyBar</bar></foo" print-element-handler)
Find element: foo
Find element: bar
SAXParseException XMLドキュメント構造は、同じエンティティ内で開始および終了する必要があります。
com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException (ErrorHandlerWrapper.java:198)

・・・という形で定義を行ったのだが、正直な話深過ぎてわかりにくい。
と思ったら、当然のごとくClojureにはそれ用の関数があるとのこと。

Javaによる継承とClojureの継承では下記の差分がある模様。

  1. 必要のないメソッドの継承は記述する必要がない → 省略された場合はUnsupportedOperationExceptionを投げるだけのメソッドとなる

このようにJavaの継承についても使用できるが、よくある場合については既にClojureがやってくれている場合も多い。
例えば、関数は自動的にRunnableとCallableを実装している。

user=> (#(println "normal"))
normal
nil
user=> (.call #(println "call"))
call
nil
user=> (.run #(println "run"))
run
nil

そのため、別のスレッドにClojureの関数を渡すのは非常に簡単となる。

user=> (dotimes [i 5]
    (.start
        (Thread.
            (fn []
                (Thread/sleep (rand 500))
                (println (format "Finished %d on %s" i (Thread/currentThread))))
)))
nil
user=> Finished 1 on Thread[Thread-1,5,main]
Finished 3 on Thread[Thread-3,5,main]
Finished 0 on Thread[Thread-0,5,main]
Finished 4 on Thread[Thread-4,5,main]
Finished 2 on Thread[Thread-2,5,main]

尚、途中でuser=>が混ざってしまうのは実際に裏側で非同期的に走っていることの証明となるだろう。

今回のようなXML解析、スレッドコールバック等の一時的なものではなく、
繰り返し使うような場合はクラスとして定義して実行すればいい。

2.ディスクへのコンパイル

まず、ClojureからJavaの継承ツリーを扱う方法について確認した。
次は、「ClojureJavaのライブラリを作る」というケースとなる。
とりあえず、書籍に乗っているAntのビルドスクリプトを読み込み、
ファイル中に定義されたタスクの名前を抽出するプログラムを確認する。ヘッダは下記の通り。

(ns reader.tasklist
  (:gen-class ; <label id="code.tasklist.genclass"/>
   :extends org.xml.sax.helpers.DefaultHandler ; <label id="code.tasklist.extends"/>
   :state state ; <label id="code.tasklist.state"/>
   :init init) ; <label id="code.tasklist.init"/>
  (:use [clojure.java.io :only (reader)])
  (:import [java.io File]
           [org.xml.sax InputSource]
	   [org.xml.sax.helpers DefaultHandler]
	   [javax.xml.parsers SAXParserFactory]))

今回は実際にファイルに出力も行うため、更に初めの宣言が増えている。
:gen-class
Clojureに「reader.tasklist」というJavaクラスを生成することを定義。
・・・だが、名前については微妙かもしれない。下記の2つの観点はあるにはあるが・・・

  1. Javaの一般的なクラス名はPascal形式で記述される
  2. JavaではなくClojureで生成したことを示すため小文字記述

ただ、ClojureJavaのクラスを生成するということはつまりJavaから利用されることを前提としていることになる。
そう考えれば、Clojure製のクラスであることを意識させることに意味はなく、そのため生成するクラス名もPascal形式でいいように思われる。
・・・というわけで、ここではPascal形式に変更した下記のヘッダとして進める。
あとファイル名もtasklist.clj → TaskList.cljにして進める。

(ns reader.TaskList
  (:gen-class ; <label id="code.TaskList.genclass"/>
   :extends org.xml.sax.helpers.DefaultHandler ; <label id="code.TaskList.extends"/>
   :state state ; <label id="code.TaskList.state"/>
   :init init) ; <label id="code.TaskList.init"/>
  (:use [clojure.java.io :only (reader)])
  (:import [java.io File]
           [org.xml.sax InputSource]
	   [org.xml.sax.helpers DefaultHandler]
	   [javax.xml.parsers SAXParserFactory]))

ここで、gen-classフォームはreader.TaskListというJavaクラスを生成することを示す模様。
同じようにextends節は親クラス、state節は状態を保持する構造体の名前を示す節、
init節はクラスの初期化関数を示す節とのこと。
初期化関数はJavaで要求されるベースクラスのコンストラクタと、Clojureで使われる最初の状態構造体・・・
て、わけわかりまへん(汗

とりあえず、main文から順にコンパイルしていくことにします。

(defn -main [& args]
    (doseq [arg args]
        (println "Received Arg is %s" arg)))

で、コンパイルコマンドを実行してみると・・

user=> (compile 'reader.TaskList)
FileNotFoundException Could not locate reader/TaskList__init.class or reader/TaskList.clj on classpath:   clojure.lang.RT.load (RT.java:443)

と、エラーが出る。
とはいえ、実はクラスパスが厳密にどう設定されているか不明確だったため確認。

user=> (use 'clojure.contrib.pprint)
user=> (pprint (seq (.getURLs (java.lang.ClassLoader/getSystemClassLoader))))
(#<URL file:/C:/Develop/Source/GitHub/clojurestudy/src/>
 #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-1.5.1.jar>
 #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/clojure-contrib-1.2.0.jar>
 #<URL file:/C:/Develop/Source/GitHub/clojurestudy/lib/criterium-0.4.0.jar>)

・・・とりあえず、src配下もクラスパスとして設定されているらしい。
そのため、srcディレクトリ配下にreaderというディレクトリを作成し、その下にTaskList.cljを移す。

内容は下記の通り。

  • C:/Develop/Source/GitHub/clojurestudy/src/reader/TaskList.clj
(ns reader.TaskList
  (:gen-class ; <label id="code.TaskList.genclass"/>
   :extends org.xml.sax.helpers.DefaultHandler ; <label id="code.TaskList.extends"/>
   :state state ; <label id="code.TaskList.state"/>
   :init init) ; <label id="code.TaskList.init"/>
  (:use [clojure.java.io :only (reader)])
  (:import [java.io File]
           [org.xml.sax InputSource]
	   [org.xml.sax.helpers DefaultHandler]
	   [javax.xml.parsers SAXParserFactory]))

(defn -main [& args]
    (doseq [arg args]
        (println "Received Arg is %s" arg)))

すると・・・

user=> (compile 'reader.TaskList)
CompilerException java.io.IOException: 指定されたパスが見つかりません。, compiling:(reader/TaskList.clj:1:1)

・・・とりあえず出力は変わったが、状況が進んだのかそうでないのかは微妙なところ。
ただ、「compiling:(reader/TaskList.clj:1:1)」と出たためファイル自体は読み込んだのか・・・?

ともあれ、長くなってきたので一度切って続きは次回に。