Common LispでGUIの必勝パターン
- Windows上で
- フリーのCommon Lispを使った
- GUIアプリケーションの
- 自分的必勝パターン
を作っておきたいと思って、ここ2,3日調べている。
結論としては、Clozure CLでCL-GTK2を使うのが今のところ一番楽でパフォーマンスも良いと思われる。
- スレッドを使えないので、Win版SBCLの優先順位は下げる。
- CLISPは遅いのでとりあえず最後の手段としておく。
開発環境構築手順
- Clozure CLをインストール http://trac.clozure.com/ccl
- GTK+2の最新ライブラリを取ってくる。 http://www.gtk.org/download-windows.html
- CL-GTK2-GTKをダウンロードしてきて展開し、ASDFでロードできるパスに加える。(詳しくは、ASDFのマニュアル参照。)
- CFFI、trivial-garbage、iterate、bordeaux-threads、closer-mopに依存しているのでこれも同様。
$HOME/ccl-init.lisp
asdf-installを一生懸命動かそうとするより、tar.gzを展開して、ASDFの.asdファイルサーチ機能を使ったほうがWindows上では楽だと思う。*1
ついでに、GTK+のライブラリのパスも通しておく。
(require 'asdf) (in-package #:asdf) (defvar *subdir-search-registry* `(,(merge-pathnames ".ccl/site/" (user-homedir-pathname)) ,(merge-pathnames ".ccl/site/cl-gtk2-0.1.1/" (user-homedir-pathname))) "List of directories to search subdirectories within.") (defvar *subdir-search-wildcard* :wild "Value of :wild means search only one level of subdirectories; value of :wild-inferiors means search all levels of subdirectories (I don't advise using this in big directories!)") (defun sysdef-subdir-search (system) (let ((latter-path (make-pathname :name (coerce-name system) :directory (list :relative *subdir-search-wildcard*) :type "asd" :version :newest :case :local))) (dolist (d *subdir-search-registry*) (let* ((wild-path (merge-pathnames latter-path d)) (files (directory wild-path))) (when files (return (first files))))))) (pushnew 'sysdef-subdir-search *system-definition-search-functions*) (in-package :cl-user) (require 'cffi) (ccl::setenv "PATH" (concatenate 'string (substitute #\/ #\\ (namestring (merge-pathnames ".ccl/libs/gtk+-bundle_2.20.0-20100406_win32/bin/" (user-homedir-pathname)))) ";" (ccl::getenv "PATH"))) (setf ccl:*default-external-format* (ccl:make-external-format :character-encoding :cp932 :line-termination :dos) ccl:*default-file-character-encoding* :utf-8 ccl:*default-socket-character-encoding* :utf-8)
プログラムを書く
(asdf:operate 'asdf:load-op :cl-gtk2-gtk) (asdf:operate 'asdf:load-op :cl-gtk2-glib) (defun hoge-main () (gtk:within-main-loop (let ((window (make-instance 'gtk:gtk-window :title "はろー"))) (gobject:connect-signal window "delete_event" (lambda (w ev) (gtk:gtk-main-quit))) (gtk:widget-show window :all t))) (gtk:join-main-thread) (ccl:quit))
EXEのビルド
wx86cl.exe --load hoge.lisp
(ccl:save-application "hogehoge.exe" :toplevel-function #'hoge-main :prepend-kernel t)
これでexeが出力される。
GTK+ライブラリのbinに入っているDLLを同じディレクトリに置いて*2、ダブルクリックすれば実行できるはず。exeが超絶でかい(33MB)のはCommon Lispの長所なので、我慢してね。
日々WindowsでExcelやPowerPointをメインに使っている一般ピーポーにもLispの恩恵を!
今後の調査課題
- 些細なことだけど、Clozure CLでsave-applicationしたexeのアイコンを変えたいので、方法を探す。
- GtkBuilder(GladeでGUIを作って読み込む)が使えれば、かなり手早くGUIが作れるので、できるかどうか試してみる。CL-GTK2のマニュアルにあるっぽいので多分できる。
- cl-win32oleを作ってる人がいるので、Excelとかの自動編集ができるGUIツールみたいなのもできるかも。
error-kit解説
clojure.contrib.error-kit*1を使うと、Common Lispのconditionに似たエラー処理ができるようになる。
Clojureのエラー処理では、Javaのtry/throw/catchをそのまま使うが、error-kitのwith-handle/raise/handle/bind-continue/continue*2を使う。
bind-continueとcontinueにより、処理を再開*3させることができるため、より柔軟なエラー処理が可能である。
まずはただの例外のように使ってみる
Javaのtry/throw/catchは、error-kitではwith-handler/raise/handleに対応する。
厳密にはいろいろ違う部分が多いが、使う用途は同じ。
- throw -> raise
- try -> with-handler
- catch -> handle
(ns errkit-sample (:use [clojure.contrib.error-kit])) (deferror not-even-error [] [x] {:msg (str "not even numner: " x)}) (defn half [x] (if (zero? (mod x 2)) (quot x 2) (raise not-even-error x))) ;ここで上げたerrorが・・・↓ (defn half-numbers [xs] (map half xs)) (defn main [] (with-handler (reduce + (half-numbers [2 4 6 8 9 10])) (handle not-even-error [x] ;ここでhandleされる。 (println "Not Even: " x))))
実行結果
=> (main) Not Even: 9 nil
普通の例外のような処理だ。
再開をしてみる
ここで、再開(continue)を使ってみよう。
handleの値としてcontinueを返すと、もう一度スタックの奥の(bind-continueした位置)に戻って再開できる。
(ns errkit-sample (:use [clojure.contrib.error-kit])) (deferror not-even-error [] [x] {:msg (str "not even numner: " x)}) (defn half [x] (with-handler ;(A) (if (zero? (mod x 2)) (quot x 2) (raise not-even-error x)) ;(1)エラーが上げられ・・・↓ (bind-continue use-value [x] ;(4)ここから再開。 x))) ;(5)with-handler(A)全体の値が0になって返る。 (defn half-numbers [xs] (map half xs)) (defn main [] (with-handler (reduce + (half-numbers [2 4 6 8 9 10])) (handle not-even-error [x] ;(2)ここでhandleされる。 (println "Not Even: " x) (continue use-value 0)))) ;(3)ここで引数0を渡して再開・・・↑
実行結果
=> (main) Not Even: 9 15
例外とerror-kitの仕組みの違い
例外(try/throw/catch)では、catchした時点でスタックが巻き戻っている。throwした時点のスタックの情報は、Throwableオブジェクトの中にしか残っていない。
これに対して、error-kitのhandleが呼び出された時点では、スタックは巻き戻っていない。
with-handlerはあくまでもスレッドローカルのリストにhandleやbind-continueの中身を関数として登録しているだけで、raiseやcontinueはそれを呼び出している。
再開処理で変わるエラー設計
上記の例をtry/throw/catchで作るには、エラー発生時に例外を投げる場合と値を置き換える場合の両方を実装するには下記のいずれかの方法をとる必要がある。
- (A)呼び出し元からフラグ変数を渡す。
- (B)別個の関数として定義する。
- (C)関数の外でフラグ変数をdefし、bindingで囲う。
(A)や(B)の方法では、half-numbersのような中間の関数にまで影響する。中間の関数にパラメータを追加したり、二つに分割したりしなければならない。(C)の方法ではエラーの定義が関数内に閉じず、別個のグローバルなシンボルを使う必要があるため、リファレンスマニュアルが汚れてわかりにくくなる。
error-kitのように再開処理が抽象化されていれば、関数のエラー処理の定義の仕方は下記のように変わる。
例外処理での関数のエラー仕様
- XXXException: XXXがおきたときにthrowされる。
- パラメータ
- XXXException/getAaaで、例外発生時のAaaの値を得ることができる。
- パラメータ
error-kitでの関数のエラー仕様
- xxx-error: xxxが起きた時にraiseされる。
- パラメータ
- 第1パラメータには、エラーraise時のaaaの値が代入されている。
- 再開
- ハンドラ内で(continue vvv a b c)を呼び出すことにより、UUUの値をWWWに置き換えることができる。
- パラメータ
いつ使うべきか
再開を使わないのならtry/throw/catchで特に問題はない。tryは3文字だが、with-handlerは12文字もある:-p
try/throw/catchとerror-kitの相互の変換も考慮されており、エラーがハンドルされなかった時にはException型*4が投げられるため、再開が必要な場所のみで使えばいいと思う。
error-kitを使ってretry
error-kitとtrampolineを使ったリトライ処理の実装。
(ns jp.t2ru.retry (:use [clojure.contrib.error-kit])) (defn- do-with-retry [f & args] (with-handler {:result (apply f args)} (bind-continue retry [& args] #(apply do-with-retry f args)))) (defn with-retry* [f & args] (:result (apply trampoline do-with-retry f args))) (defmacro with-retry [bindings & body] (let [bind-pairs (partition 2 bindings) bind-syms (map first bind-pairs) bind-vals (map second bind-pairs)] `(with-retry* (fn ~(vec bind-syms) ~@body) ~@bind-vals))) (defmacro with-retry-handler [bindings & body] (let [handler-labels #{'handle 'bind-continue} [main-forms handle-forms] (split-with #(not (handler-labels (first %))) body)] `(with-handler (with-retry ~bindings ~@main-forms) ~@handle-forms))) (defmacro retry [& args] `(continue ~'retry ~@args)) (comment (deferror hoge [] [x]) (defn t [] (with-retry-handler [a 1] (if (< a 10) (do (prn "Next" a) (raise hoge a)) (do (prn "Finish" a) a)) (handle hoge [x] (prn "handling" x) (retry (inc x))))) )
forマクロ
Clojureでプログラムを組んでいるうちに、forマクロが強力だということにやっと気づいたので、リファレンスを書いてみる。
forマクロとは
普通の(というか、CやJava系列の)言語では、forはループ文を表すが、Clojureではforマクロはループではなく、シーケンスを作るオペレータだ。
下記のコードでは、0〜9までの数をそれぞれ2倍した数のシーケンス(0, 2, 4, ..., 18)を返す。
(for [x (range 10)] (* x 2))
しかも、返されるシーケンスは遅延シーケンスなので、この式が評価されたときに中身が評価されるのではなく、シーケンスの中身が評価された時に評価される。
=> (let [x (for [x (range 3)] (do (println "x=" x) (* x 2)))] (prn "hoge") (prn x)) "hoge" (x= 0 x= 1 x= 2 0 2 4) nil
複数のシーケンス
複数シーケンスの指定は、多重ループに似た感じになる。
=> (for [x (range 3) y (range 3)] [x y]) ([0 0] [0 1] [0 2] [1 0] [1 1] [1 2] [2 0] [2 1] [2 2])
こんなのも可能。
=> (for [x [[1 2 3] [4 5 6] [7 8 9]] y x] y) (1 2 3 4 5 6 7 8 9)
:whenキーワード
上記のままだと、単なるmapの代わりだが、:whenキーワードを使うとfilter相当のこともできる。
=> (for [x (range 10) :when (even? x)] x) (0 2 4 6 8)
:whileキーワード
take-whileのようなこともできる。
=> (for [x (range 1000) :while (not= x 10)] x) (0 1 2 3 4 5 6 7 8 9)
:letキーワード
=> (for [x (range 10) :when (< (* x 2) 15)] (* x 2)) (0 2 4 6 8 10 12 14)
これでは、(* x 2)が2回計算しなければならず、効率が悪いので、、、
=> (for [x (range 10) :let [y (* x 2)] :when (< y 15)] y) (0 2 4 6 8 10 12 14)
:letを使えば、1回で済む。
複数のリストを同時に取り出す
forの場合、(for [x coll1 y coll2] ...)は2重ループ相当だが、mapのように複数のリストから同時に値を引き出したいときもある。
少し汚いかも知れないが、map listを使うのが簡単ではないかと思う。
=> (for [[x y] (map list [:a :b :c] [1 2 3])] [x y]) ([:a 1] [:b 2] [:c 3])
identityの別名定義でmapを便利にする
Clojureのmapでこういう変換をしたくなるときがよくある。
(1 "piyo" :foo) -> ({:hoge 1} {:hoge "piyo"} {:hoge :foo})
でも、#()で無名関数の定義をしてみると、エラーが出る。
#()の中身は、関数呼び出しorマクロor特殊形式である必要があるため、(#(1))とかやると、「IFnじゃないから呼び出せない」と怒られる。
(map #({:hoge %}) [1 2 3]) -> java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap
マップ({})は、元々IFnを継承しているが、引数の数が1個か2個とるので、上記だと「引数の数が違う」と言われてしまう。
下記のようにすれば一応いけるが、もっと簡潔にできそう。
(map (fn [x] {:hoge x}) [1 2 3]) -> ({:hoge 1} {:hoge 2} {:hoge 3})
#()を使うには、引数をそのまま返す関数identityが使える。
(map #(identity {:hoge %}) [1 2 3]) -> ({:hoge 1} {:hoge 2} {:hoge 3})
このままだと、かえって長くなっているので、ideneityの別名を定義する。
(def | identity)
すると、下記のように書ける。
(map #(|{:hoge %}) [1 2 3]) -> ({:hoge 1} {:hoge 2} {:hoge 3})
多少は簡単になったかな?しばらく使ってみよう。
でも、「|」ではない別のもっと良い記号があるかもしれない。
Leiningenで簡単Clojureプロジェクト
Programming Clojureの訳本がもうすぐ出るということで、ここ数日盛り上がり気味のClojure。Amazonに出たので、即注文。
Clojureには、Javaで言うMavenに相当するツールで「Leiningen」というのがある。
http://github.com/technomancy/leiningen
これを使えば、依存ライブラリのダウンロードから、スタンドアローンのjar生成まで一気にやってくれる。
インストール
公式サイトの説明そのままだけど、インストール方法。
# シェルスクリプトをダウンロードしてパスの通ったところに置く。 $ wget -O /usr/local/bin/lein http://github.com/technomancy/leiningen/raw/stable/bin/lein # 実行権限をつける。 $ chmod a+x /usr/local/bin/lein # ~/.m2/の下にleiningenがインストールされて、すぐに使えるようになる。 $ lein self-install
Clojureを単体でインストールするよりも簡単かも。
(ただし、Windowsのbatはまだないので、自力でインストールする必要がある。)
プロジェクト作成
lein new [project-name]
これで、[project-name]の下に project.cljとREADME、src/[project-name].cljができる。
あとは、[project-name].cljにclojureのプログラムを書くだけ。
他のライブラリを使いたければ、適当なのをhttp://clojars.org/で探してきて、project.cljの中に書く。
大抵は、:dependenciesのところに [グループID/アーティファクトID "バージョン"]と書くだけで大丈夫。
普通のMavenリポジトリも使える。
サンプル:
(defproject myproject "0.0.1-SNAPSHOT" :description "hogehoge" :dependencies [[org.clojure/clojure "1.1.0-alpha-SNAPSHOT"] [org.clojure/clojure-contrib "1.0-SNAPSHOT"] [compojure "0.3.2"] ;ClojarsにあるCompojureを使う。 [jfree/jfreechart "1.0.13"]] ;JBossのリポジトリにあるやつを使う。 :reposotiries [["jboss" "http://repository.jboss.com/maven2/"]] :main myproject ;これを指定してやると、myproject.cljの(def -main [] ...)の箇所が ;スタンドアローンjar(下記)でのエントリポイントになる。
普通のMavenリポジトリも使えるため、このへん→http://mvnrepository.com/で適当に探してもよい。
JBossのリポジトリhttp://repository.jboss.com/maven2/とかなら、Web系の最新のライブラリが手に入る。
スタンドアローンjar作成
依存するライブラリを全部まとめて、一つのjarファイルに固めて配布できる。
JavaがインストールされているWindows上にコピーして、ダブルクリックすると、project.cljの:mainで指定したnamespaceの(def -main [] ...)から実行が始まるファイルを作ることができる。
; (project.cljに:mainを指定した上で) lein uberjar
これで、[project-name]-standalone.jarが生成される。
その他
- コマンド一発(二発?)でClojarsに自分の作ったライブラリを上げることができるらしい。
- 社内ローカルにclojarsサーバを立ててライブラリを共有する、みたいな使い方もできそうなので試してみたい。
- やれることはmavenと一緒だが、簡単さが重要。
[Clojure]Programming Clojureの感想
Programming Clojureを読んだのでその感想、の前に、Clojureの特徴など。
Clojureとは
私見ですが、Clojureの特徴を紹介します。
Lispの方言
Common LispやSchemeの仕様とは互換性はない。Common Lispに比べてなじみやすい関数名になっていたり、Lisp-2ではなくLisp-1だったりする。Schemeに比べればCommon Lispに近い*1機能を持ったdefmacroが用意されている。
JavaVM上で動く
関数型プログラミングへの誘導
ミュータブルな操作は可能だが、自然にプログラムを組んでいれば関数型のプログラムになるように注意深く設計されている。
どういうことかというと、使いやすいところにイミュータブルなデータ構造が置かれていて、ミュータブルなオブジェクトを扱うときには専用のAPIを通さないといけないようになっているから。で、そのAPIがErlangプロセスやScalaアクターと同じくマルチコア時代対応(?)仕様になっているので、自然と別スレッドに対する考慮をプログラマに意識させるようになっている。
- ミュータブルなデータ構造を扱うAPI
トランザクションの制御方法は、STM (Software Transactional Memory)というアイデアを使っている。
周辺ライブラリ
初出が2006年ということもあり、周辺ライブラリはまだまだかなーと思いきや、、、結構使えるのが既にそろっている。
Web/DBシステムを作るのに必要な、DBアクセス(ClojureQLなど)、Webフレームワーク(Compojureなど)があって、ダウンロードして配置するだけであっさり動いてくれた。
デバッグ
JavaVM上で動作する非Java言語にありがちなのが、スタックトレースが意味不明という現象。
Clojureにはclojure.stacktraceというJavaVMスタックトレースをClojure仕様に翻訳してくれるライブラリがあり、ある程度緩和はされている。しかし、Eclipse上のデバッガではJavaVMの生のstacktraceが出るため、だいぶわかりづらい。無名関数を多用しているコードはただでさえスタックトレースを追いづらい上に、Clojureのライブラリ層がスタックのいたるところに出てくる。
あと、Lispのプログラムは横に長くなる傾向があるので、スタックトレースにソースコードの行数だけでなく、桁数も表示できればと思うんだけど、Java APIの仕様を見ている限りにおいてはできなさそう。
まあ、Clojureに限らず言えることですかね。