jarファイルをまとめてクラスパスに追加する
clojureでいろいろとコードを書き始めると、多種のjavaのライブラリを使うことになると思うが、いちいち.emacsにライブラリを追加するのは面倒だ。
Emacs Lispのdirectory-files関数を使うと指定したディレクトリにあるjarファイルをリストとして取得できるので、swank-clojure-extra-classpathsを以下のように記述すると、
~/opt/compojure/deps
~/opt/libs
にあるjarライブラリをまとめて追加できる。
.emacsのクラスパス設定部分
(setq swank-clojure-jar-path "~/opt/clojure/clojure.jar" swank-clojure-extra-classpaths (append (list "~/opt/swank-clojure/src/main/clojure" "~/opt/apache-ant/lib/ant.jar" "~/opt/apache-ant/lib/ant-launcher.jar" "~/opt/clojure-contrib/clojure-contrib.jar" "~/opt/compojure/compojure.jar") (directory-files "~/opt/compojure/deps" t "\.jar$") (directory-files "~/opt/libs" t "\.jar$") (list "~/workspace/clojure/src" "~/workspace/clojure/classes")))
antでcljファイルをコンパイル(build.xmlのひな形)
antでcljファイルをコンパイル後、jarにアーカイブするためのbuild.xmlを作成した。
プロジェクト構成
プロジェクトのディレクトリ構成は以下の通り。
hogeapp/ … プロジェクトディレクトリ src/ … ソースファイル sample/hoge/hogeapp.clj … サンプルアプリ classes/ … クラスファイル lib/ … ライブラリ clojure.jar clojure-contrib.jar hogeapp.jar
サンプルアプリ
コマンドライン引数を標準出力に出力する簡単なアプリ。
(ns hogeapp (:gen-class)) (defn -main [& args] (doseq [arg args] (println arg)))
build.xml
配布と実行が簡単にできるように、libディレクトリにあるclojure.jar,clojure-contrib.jarもまとめてアーカイブし、実行可能なjarを作成する。
<?xml version="1.0" encoding="UTF-8"?> <project name="hogeapp" default="all"> <description> Sample build.xml for *.clj files. </description> <property name="src" value="src"/> <property name="classes" value="classes"/> <property name="lib" value="./lib"/> <property name="destfile" value="hogeapp.jar"/> <property name="mainclass" value="sample.hogeapp"/> <target name="init"> <mkdir dir="${classes}"/> </target> <target name="compile" depends="init"> <java classname="clojure.lang.Compile"> <classpath> <fileset dir="${lib}"> <include name="*.jar"/> </fileset> <pathelement path="${src}:${classes}"/> </classpath> <sysproperty key="clojure.compile.path" value="${classes}"/> <arg value="sample.hogeapp"/> </java> </target> <target name="jar" depends="compile"> <jar destfile="${destfile}"> <zipfileset dir="${classes}"/> <!-- lib配下のjarもまとめてアーカイブする --> <zipgroupfileset dir="${lib}" includes="*.jar"/> <manifest> <attribute name="Main-Class" value="${mainclass}"/> </manifest> </jar> </target> <target name="all" depends="jar"> </target> <target name="clean"> <delete dir="${classes}"/> </target> </project>
cljファイルのコンパイルにはclojure.jarに含まれるclojure.lang.Compileクラスを使用する。compileターゲットに記述したように、javaタスクを使用してコンパイルする。
javaタスクに設定する属性と子要素
要素 | 属性 | 値 |
---|---|---|
java | classname | clojure.lang.Compile |
+classpath | クラスパスを設定(※srcとclassesも必要) | |
+sysproperty | key | clojure.compile.path |
value | classes | |
+arg | value | コンパイルするcljファイル(※拡張子は不要) |
cljファイルが複数の場合は、そのファイル分だけarg要素を追加する。
ビルドと実行
ビルドしてみる。
satoshi@ubuntu:~/workspace/hogeapp$ ls build.xml lib src satoshi@ubuntu:~/workspace/hogeapp$ ant Buildfile: build.xml init: [mkdir] Created dir: /home/satoshi/workspace/hogeapp/classes compile: [java] Compiling sample.hogeapp to classes jar: [jar] Building jar: /home/satoshi/workspace/hogeapp/hogeapp.jar all: BUILD SUCCESSFUL Total time: 12 seconds satoshi@ubuntu:~/workspace/hogeapp$ ls build.xml classes hogeapp.jar lib src ←◆hogeapp.jarができた
さて、実行だ。
satoshi@ubuntu:~/workspace/hogeapp$ java -jar hogeapp.jar one two three one two three
動いた。\(^▽^)/
echo server
Programming Clojureを読み終えた。
Common Lispとは微妙に違うので、コードを書く時にとまどう事も多いが、豊富なJavaライブラリを直接使用できるのは大きな利点である。
emacs + slimeを使えば、動作確認をしながらコードを書けるので、サクサク開発できて、なかなか便利である。
一通りの機能は理解できたので、いろいろ書いてみようと思う。
ネットワーク関係が面白いので、まずはecho serverを作ってみた。
sample/echo.clj
(ns sample.echo (:use [clojure.contrib.server-socket :only (create-server close-server)])) (def port-no 3000) (defn echo [in out] (let [caption (str "*echo(" (.getId (Thread/currentThread)) ") ")] (println (str caption "start")) (let [buf (make-array Byte/TYPE 256)] (loop [] (let [size (.read in buf)] (when (not= size -1) (.write out buf 0 size) (println (str caption "loop")) (recur))))) (println (str caption "end")))) (def echo-server (ref nil)) (defn start-echo-server [] (dosync (ref-set echo-server (create-server port-no echo)))) (defn stop-echo-server [] (close-server @echo-server) (dosync (ref-set echo-server nil)))
start-echo-serverを実行し、3000番ポートにtelnetすると、入力した文字列をそのまま返す。
サーバ側
user> (use 'sample.echo) nil user> (start-echo-server) {:server-socket #<ServerSocket ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=3000]>, :connections #<Ref@18f127c: #{}>} *echo(73) start ←◆接続した *echo(73) loop ←◆hogeを送信 *echo(73) loop ←◆fugaを送信 *echo(73) end ←◆切断した *echo(74) start
クライアント側
satoshi@ubuntu:~$ telnet localhost 3000 Trying ::1... Connected to localhost. Escape character is '^]'. hoge hoge fuga fuga ^] telnet> quit Connection closed.
終了するには、stop-echo-serverを実行する。
Clojureでは、clojure.contrib.server-socketライブラリが用意されており、このようなサーバアプリを簡単に作成できる。
上記のサンプルのように、create-server関数にポート番号と、入力ストリームと出力ストリームを引数とする関数を渡すだけで、マルチスレッドサーバを書くことができる。
ソケットの入出力ではbyte配列を使用するが、byte配列を作るにはmake-array関数を使う。
make-array関数に渡すtype引数は、byteプリミティブ型の場合、Byte/TYPEを指定する。
ちなみに、intの場合はInteger/TYPEを指定する。
例) 長さ10のbyte配列を作る。
(make-array Byte/TYPE 10)
上記と同様なecho serverのJavaによる実装は以下の通り。
sample/echo/EchoServer.java
package sample.echo; import java.io.IOException; public class EchoServer { private static final int PORT_NO = 3000; public static void main(String[] args) throws IOException { EchoServer echoServer = new EchoServer(); echoServer.start(); } private void start() throws IOException { ServerSocket serverSocket = new ServerSocket(PORT_NO); System.out.println(String.format("*ポート番号%dで待機しています。", PORT_NO)); while (true) { Socket socket = serverSocket.accept(); System.out.println(String.format("*接続しました(%s:%d))。", socket .getInetAddress(), socket.getPort())); Thread echoThread = new Thread(new Echo(socket)); echoThread.start(); } } }
sample/echo/Echo.java
package sample.echo; import java.io.IOException; public class Echo implements Runnable { private Socket socket; public Echo(Socket socket) { this.socket = socket; } @Override public void run() { final String caption = String.format("*echo(%d) ", Thread.currentThread().getId()); System.out.println(caption + "start"); try { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); int size; byte[] buf = new byte[256]; while ((size = in.read(buf)) != -1) { System.out.println(caption + "loop"); out.write(buf, 0, size); } socket.close(); System.out.println(caption + "end"); } catch (IOException e) { e.printStackTrace(); } } }
12/13追記
おぉ、いかん。socket.close()をfinallyにしていなかった。
アナフォリックマクロ
Programming ClojureのChapter 7 Macroの途中まで読んだので、マクロを使ってみた。
アナフォリックマクロを作成しようとして、
sample/macro.clj
(ns sample.macro) (defmacro aif [pred then] `(let [it ~pred] (if it ~then)))
を定義し、実行すると、
user> (use 'sample.macro) nil user> (aif (+ 1 2 3) (println it)) ; Evaluation aborted. Can't let qualified name: sample.macro/it [Thrown class java.lang.Exception]
エラーとなる。
Clojureでは、マクロ展開時のシンボル補足によるバグを防ぐため、「`」でクオートされた構文中にシンボルが表われた場合は、そのシンボルはqualified nameになる。
上記の aifの場合は、it は sample.macro/it となるため、エラーとなる。
user> (macroexpand '(aif (+ 1 2 3) (println it))) (let* [sample.macro/it (+ 1 2 3)] (if sample.macro/it (println it)))
アナフォリックマクロのように、シンボル補足を利用するマクロの場合は、補足したいシンボルに~'を付ければ良い。
(defmacro aif ([pred then] `(aif ~pred ~then nil)) ([pred then else] `(let [~'it ~pred] (if ~'it ~then ~else))))
user> (aif (+ 1 2 3) (println it)) 6 nil user> (aif false (println it) (println it)) false nil
動いた。\(^▽^)/
agent同期化のサンプル
agentをawait,await-for関数で同期化するサンプルを書いてみた。
send関数によるagentの更新は非同期で行なわれるが、await関数を使い、send関数で指示した関数の実行が完了するまで待機することができる。
待ち時間を指定したい場合はawait-for関数を使用する。
agent-sample.clj
; カウンタを初期値0のagentとして定義 (def counter (agent 0)) ; 1秒待機した後、引数を1増やした値を返す関数 (defn count-fn [x] (Thread/sleep 1000) (inc x)) ; 以下の処理を10回繰り返す。 ; (1)カウンタ値を増やす関数をsend関数で呼び出す。 ; (2)awaitまたはawait-forで(2)のsend関数によるagent更新が完了するまで待機する。 (dotimes [i 10] (send counter count-fn) (await counter) ; (await-for 500 counter) (println (format "@counter=%d" @counter)))
awaitの場合の実行結果
await関数により、カウンタ値の更新が同期化されるため、カウンタ値は1ずつ増える。
user> (load-file "agent-sample.clj") @counter=1 @counter=2 @counter=3 @counter=4 @counter=5 @counter=6 @counter=7 @counter=8 @counter=9 @counter=10 nil user> @counter 10
await-forの場合の実行結果
await-forには500msecを指定しているため、カウンタ値は更新前の値となる。
user> (load-file "agent-sample.clj") @counter=0 @counter=0 @counter=1 @counter=1 @counter=2 @counter=2 @counter=3 @counter=3 @counter=4 @counter=4 nil user> @counter ←5秒ほど待ってから入力 10
alterとcommute
Programming Clojureをcommuteの説明まで読んだので、alterとcommuteの違いが分かるようなサンプルプログラムを書いてみた。
test.clj
(def *counter* (ref 0)) (defn test-func [name time] (fn [] (dotimes [i 5] (Thread/sleep 1000) (print (format "%d: I am %s. " i name)) (dosync (print ".") (.flush *out*) (alter *counter* inc) ; (commute *counter* inc) (println (format " --> %d" @*counter*)) (when time (Thread/sleep time)))) (println (format "%s end" name)))) (def *t1* (Thread. (test-func "Foo" 1000))) (def *t2* (Thread. (test-func "Bar" nil))) (do (.start *t1*) (.start *t2*))
何をしているかと言うと、
スレッド*t1*と*t2*を作る。
それぞれのスレッドから同期化してカウンタを1増やす。
スレッド1では、以下の処理を5回繰り返す。
- 1秒待つ。
- 「I am Foo.」 を出力する。
- トランザクション開始
- 「.」 を出力する。
- カウンタを増やす。※alterまたはcommuteを使用する。
- 「--> カウンタ値」を出力する。
- 1秒待つ。
スレッド2は、「I am Bar.」を出力し、最後の1秒待つは無し。
関数はRunnableインターフェースを実装しているため、スレッドを書くのは簡単だ。
alterの場合の実行結果
user=> (load-file "test.clj") nil user=> 0: I am Foo. . --> 1 0: I am Bar. ........... --> 2 1: I am Foo. . --> 3 1: I am Bar. ........... --> 4 2: I am Foo. . --> 5 2: I am Bar. ........... --> 6 3: I am Foo. . --> 7 3: I am Bar. ........... --> 8 4: I am Foo. . --> 9 4: I am Bar. ..........Foo end . --> 10 Bar end
※スレッド内で標準出力に出力しているため、slimeでは出力されない事に注意(出力できるかもしれないが、その方法は知らない)。
2009/11/22 訂正 スレッドからの出力は*inferior-lisp*バッファに出力される。
alter実行時に他のトランザクションにより、参照が更新されている場合はトランザクションをリトライする。
カウント値は更新後の値を表示する。
おおよそ100msec間隔でリトライしているようであるが、どこかで設定可能?
commuteの場合の実行結果
user=> (load-file "test.clj") nil user=> 0: I am Bar. . --> 1 0: I am Foo. . --> 2 1: I am Bar. . --> 2 2: I am Bar. . --> 4 1: I am Foo. . --> 5 3: I am Bar. . --> 5 4: I am Bar. . --> 7 Bar end 2: I am Foo. . --> 8 3: I am Foo. . --> 9 4: I am Foo. . --> 10 Foo end
Programming Clojureに書いてあるように、commute実行時は参照の更新順をSTMシステム任せにするため、上記の実行結果のようにトランザションはリトライしない。
出力されるカウント値はトランザクション終了時の値とはならない。
共有オブジェクトの更新時に、他トランザクションにより更新されていると、トランザクションをリトライするという仕様にちょっと驚いた。
clojure.contrib.def
clojure.contrib/def.cljのソースを眺めたのでメモ。
名前 | 説明 |
---|---|
defvar | 変数を初期値とドキュメント文字列で定義する。 |
defunboud | unbound変数をドキュメント文字列で定義する。 |
defmacro- | defmacroのプライベート定義版。 |
defvar- | defvarのプライベート定義版。 |
defunbound- | defunboudのプライベート定義版 |
defstruct- | defstructのプライベート定義版 |
defone- | defonceのプライベート定義版 |
defalias | 変数のエイリアスを定義する。メタデータには元の変数のメタデータがマージされる。 |
defhinted | 初期値のクラスを型のヒントとして変数を定義する。 |
name-with-attributes | マクロ引数の定義で使用できる。ドキュメント文字列やメタデータを簡単に定義できる(ようだ)。 |
defnk | キーワード引数を扱える関数を定義する。 |
def*-と最後にハイフンが付くdef*を使用すると、定義した名前空間だけで有効な変数(プライベートな変数)を定義できる。
キーワード引数が使用できるdefnkは便利そうなので、後で詳細を調べてみよう。(Common Lispと同様?)