Common LispでGUIの必勝パターン

  • Windows上で
  • フリーのCommon Lispを使った
  • GUIアプリケーションの
  • 自分的必勝パターン

を作っておきたいと思って、ここ2,3日調べている。

結論としては、Clozure CLでCL-GTK2を使うのが今のところ一番楽でパフォーマンスも良いと思われる。

  • スレッドを使えないので、Win版SBCLの優先順位は下げる。
  • CLISPは遅いのでとりあえず最後の手段としておく。

開発環境構築手順

  1. Clozure CLをインストール http://trac.clozure.com/ccl
  2. GTK+2の最新ライブラリを取ってくる。 http://www.gtk.org/download-windows.html
  3. CL-GTK2-GTKをダウンロードしてきて展開し、ASDFでロードできるパスに加える。(詳しくは、ASDFのマニュアル参照。)
    1. 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ツールみたいなのもできるかも。

*1:MinGWからとってきたtarコマンドではgzipをforkできないっぽいし、Cygwinをわざわざ入れるのも面倒。(もちろん、自分のPCには普通に入っているけど、会社のいろんなPCでやることを想定しないといけない。)

*2:DLLが別置きだと、古いライブラリをリンクしてしまったりして動かない可能性がある

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が投げられるため、再開が必要な場所のみで使えばいいと思う。

*1:公式にはEXPERIMENTALとあり、APIは今後変わるかもしれないので注意。

*2:ここでいうcontinueは、call/ccのcontinuationとは何の関係も無い。

*3:continueを「再開」と訳すのはあまりよくないかもしれないが、他に良い訳語が思いつかなかった。ちなみに、Practical CommonLispでは「再起動」と言っている。

*4:例外の型はdeferrorの:unhandledでカスタマイズできる。

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が生成される。

REPLの実行

もちろん、REPLもできる。

lein repl

とやれば、依存するファイルにclasspathを全部通したREPLを使うことができる。
楽ちん過ぎる。

その他

  • コマンド一発(二発?)でClojarsに自分の作ったライブラリを上げることができるらしい。
    • 社内ローカルにclojarsサーバを立ててライブラリを共有する、みたいな使い方もできそうなので試してみたい。
    • やれることはmavenと一緒だが、簡単さが重要。

[Clojure]Programming Clojureの感想

Programming Clojureを読んだのでその感想、の前に、Clojureの特徴など。

Clojureとは

私見ですが、Clojureの特徴を紹介します。

Lispの方言

Common LispSchemeの仕様とは互換性はない。Common Lispに比べてなじみやすい関数名になっていたり、Lisp-2ではなくLisp-1だったりする。Schemeに比べればCommon Lispに近い*1機能を持ったdefmacroが用意されている。

JavaVM上で動く
  • 長年たたき上げられたJavaVMの資産を活用できる。
    • 効率的なネイティブコードを作ってくれるJust-in-Timeコンパイラ
    • 性能の良くなったGC
    • 無数のライブラリ
  • Javaのクラスやメソッドが言語内に組み込まれているかのように呼び出せる。
    • 呼び出しが簡単
      • (new fqcn.MyClass arg1 arg2 ...)
      • (. obj method arg1 arg2 ...)
    • Javaクラスを継承したオブジェクトもOn-the-Flyで作れる
      • (proxy [SuperClass Intefaces1 Interface2 ...] [super-ctor-arg1 ...] (method-name [meth-arg1 ...] ..body..) ...)
      • 実行時にその場でバイトコードにコンパイルされてロードされる*2
関数型プログラミングへの誘導

ミュータブルな操作は可能だが、自然にプログラムを組んでいれば関数型のプログラムになるように注意深く設計されている。
どういうことかというと、使いやすいところにイミュータブルなデータ構造が置かれていて、ミュータブルなオブジェクトを扱うときには専用のAPIを通さないといけないようになっているから。で、そのAPIErlangプロセスやScalaアクターと同じくマルチコア時代対応(?)仕様になっているので、自然と別スレッドに対する考慮をプログラマに意識させるようになっている。

  • ミュータブルなデータ構造を扱うAPI
    • Refs(ref, deref, dosync, ref-set, alter, commute)
      • 複数のミュータブルなオブジェクトの参照/更新をトランザクションとして扱える。(ACIDのうち、ACIが保障されている。)
    • Atoms
      • 単一のオブジェクトを扱えれば良い場合は、Refsよりも性能の良いAtomsが使える。
    • Agents
      • 値の更新が同期でなくて良い場合は、RefではなくAgentsでもいい。
    • Javaクラス
      • Javaのイミュータブルに実装されたクラスライブラリは、そのまま扱える。

トランザクションの制御方法は、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に限らず言えることですかね。

感想

全体的に関数型プログラミングLispJavaライブラリ資産を統合するという観点で、絶妙なバランス感覚の仕様になっていると感じた。
ClojureほどきれいにLispJavaを統合できるのを見ると、Scalaは不必要にJava言語に近すぎるんじゃないかとさえ思えてきた。

みたいな感じ。

仕事でも使ってみているが、仕事場にLisperがほとんど皆無という厳しい環境下にあるので、他人がメンテする可能性の無いものに限っている。あまりにもよさげなので、勉強会でも開いて広めてみてもいいかも。

書籍Programming Clojureの感想

Lisper、Javaプログラマ、いわゆる各種LLのプログラマそれぞれにターゲットをおいて書かれている。
サンプルはJavaライブラリの利用にもきっちりフォーカスを当てている。
英語が大の苦手にもかかわらず、特に問題なく読めたほどわかりやすい。

日本語訳が近々出るとのうわさ。

Clojure使うあての無いPerl, Python, Ruby, JavaScript, etc...プログラマであっても、興味深い領域をわかりやすく知ることができるので、超おすすめ。
isbn:1934356336

*1:展開時のシンボルの扱いなどがちょっと違うみたい。

*2:ループでぶん回すとPermGenがいっぱいになってしまわないか?その辺、考慮されていないわけは無いとは思うが、要検証