本を読む

読書やコンピュータなどに関するメモ

「プログラミングClojure」

プログラミングClojure
プログラミングClojure
posted with amazlet at 10.02.24
Stuart Halloway
オーム社
売り上げランキング: 8549

 本書でClojure言語を面白く学んだ。書籍としても、日本語がこなれていて読みやすい。

 Clojureは、JavaVMで動くLispで、変更不可能(immutable)なデータによる並行関数型言語だそうな。言語仕様の組み合わせはもちろん、それ以上に割り切りのセンスがいいと思った。

 よーし、GAE/JでClojureとcompojureを使って並行処理をキメちゃうぞ、とか思ったけど、GAE/Jではスレッドを作れないのであった。

 以下、本書を見ながら手を動かして、ほかのLisp(主にSchemeとかCommon Lispとか)との差分について部分的に写経したメモ。

インストールと起動

 これを書いてる時点でDebian sidにパッケージがある。

$ sudo apt-get install clojure

 依存関係で、openjdk6等が入ってなければ入る。

 REPLを起動。

$ clojure-repl
WARNING: clojure.lang.Repl is deprecated.
Instead, use clojure.main like this:
java -cp clojure.jar clojure.main -i init.clj -r args...
Clojure 1.1.0
user=> 

関数とか値とか

 関数はdefnで定義。仮引数はリストじゃなくてベクタ。

user=> (defn plusone [n] (+ n 1))
#'user/plusone
user=> (plusone 3)
4

 束縛自体もvarという型の値。varは#'hogeまたは(var hoge)で参照できる。userは標準の名前空間。

 無名関数はlambdaじゃなくてfn。

user=> ((fn [n] (+ n 1)) 3)
4

 または#()。この場合、引数は1つなら%、複数なら%1、%2…。

user=> (#(+ % 1) 3)
4
user=> (#(+ %1 %2) 3 5)
8

 値はdefで定義。

user=> (def foo 51)
#'user/foo
user=> foo
51
 ClojureはLisp 1。
user=> (def plusone (fn [n] (+ n 1)))
#'user/plusone
user=> (plusone 3)
4

シーケンス、遅延評価

 carとcdrのかわりにfirstとrest。

user=> (first '(a b c))
a
user=> (rest '(a b c))
(b c)

 リスト以外の「シーケンス」なデータ型も同じように扱える。

user=> (first [1 2 3])
1
user=> (rest [1 2 3])
(2 3)
user=> (first "abc")
\a
user=> (rest "abc")
(\b \c)

 シーケンスとして抽象化された値(上の返り値など)をREPLが表示するときには、仮にリストとして見える。

 シーケンスは遅延評価。iterate関数は第1引数の関数を順に適用した無限シーケンスを返す。takeは先頭からn個の値を返す。

user=> (take 3 (iterate inc 1))
(1 2 3)

 ちなみに()とnilは別。

user=> (= () nil)
false

制御構造的なナニカ

 condはカッコのレベルが1つ少なく、条件と評価する式の交代リスト。関数型なので暗黙のprognは不要ということか。

user=> (cond (< foo 50) 'low (> foo 50) 'high)
high

 prognやbeginに相当するのはdo。副作用目的。Clojureではdoほげほげという名前は副作用を意識したものにつける慣習らしい。

user=> (do 1 2)
2

 「..」は、値を順番に関数の引数にする。Haskellの$みたいというか、アナフォリックマクロ的なことをメソッドチェーンやパイプ風にやるマクロ。

user=> (.. '((a b) c) (first) (first))
a

 loopとrecurは、無名の名前付きlet(?)というところ。

user=> (defn sum-to-0 [n] (loop [r 0 i n] (if (zero? i) r (recur (+ r i) (- i 1)))))
#'user/sum-to-0
user=> (sum-to-0 3)
6

 loopなしだとrecurは関数自身の再帰に。

user=> (defn sum-to-0 [r i] (if (zero? i) r (recur (+ r i) (- i 1))))
#'user/sum-to-0
user=> (sum-to-0 0 3)
6

 Clojureでは関数は末尾再帰などの最適化はしないが、recurなら最適化する。Java VMで実行しやすいようにとか。再帰やループよりシーケンスを使うのがClojure流、ということらしい。

 forは制御構造ではなくシーケンスの内包表記。

user=> (take 5 (for [n (iterate inc 1)] (+ n 10)))
(11 12 13 14 15)

 シーケンスは遅延評価なので、下のprintlnはhogeの値がREPLなどに参照されるまで実行されない。

user=> (def hoge (for [n (range 1 3)] (println n)))
#'user/hoge

 すべての評価を強制するには、値が必要ならdoall、不要ならdorun。

user=> (def hoge (dorun (for [n (range 1 3)] (println n))))
1
2
#'user/hoge

マップ型、セット型

 ほかの言語でハッシュとかハッシュテーブルとかディクショナリとか呼ぶデータ構造は、closureではマップ型。

user=> (def m {:foo 51 :bar 52})
#'user/m

 Clojureでは「,」は空白なので、こう書いても同じ。

user=> (def m {:foo 51, :bar 52})
#'user/m

 ちなみに、構文クォート(準クォート)では「,」「,@」のかわりに「~」「~@」を使う。

 マップ型は関数として参照できる。

user=> (m :foo)
51

 RubyとかPythonとかにあるようなセット型もある。

user=> (def s #{3 5 7})
#'user/s
user=> (s 3)
3
user=> (s 4)
nil

メタデータ

 オブジェクトにはメタデータというデータを付けられる。Lispでシンボルにつくプロパティリストを汎用的にしたようなもの?

user=> (def foo (with-meta 'tom {:age 21}))
#'user/foo
user=> (meta foo)
{:age 21}

 本書の本文では^fooという表記(リーダーマクロ)が使われているけど、訳注にもあるとおり古い仕様なので、使うと警告が出る。

user=> ^foo
WARNING: reader macro ^ is deprecated; use meta instead
{:age 21}

 関数定義などのvarにdoc-stringや型定義を付けられる。これもメタデータ。

user=> (defn plusone "returns n plus 1" [n] (+ n 1))
#'user/plusone
user=> (doc plusone)
-------------------------
user/plusone
([n])
  returns n plus 1
nil
user=> (meta #'plusone)
{:ns #<Namespace user>, :name plusone, :file "NO_SOURCE_PATH", :line 25, :arglists ([n]), :doc "returns n plus 1"}

Javaとのつながり

 Clojureのデータ型はJavaのクラスと一対一対応。Javaの標準クラスで表現できるものは表現する。

user=> (class "abc")
java.lang.String

 Javaのメソッドの呼び出し。

user=> (. "abc" toUpperCase)
"ABC"
user=> (.toUpperCase "abc")
"ABC"

 new。

user=> (def rnd (new java.util.Random))
#'user/rnd
user=> (def rnd (java.util.Random.))
#'user/rnd

 staticメソッド。

user=> (. Math PI)
3.141592653589793
user=> (Math/PI)
3.141592653589793

マルチメソッド

 総称関数。defmultiで名前とディスパッチを定義し、defmethodで処理を実装する。

user=> (defmulti num-or-str class)
#'user/num-or-str
user=> (defmethod num-or-str Number [n] "It's a number")
#<MultiFn clojure.lang.MultiFn@1774b9b>
user=> (defmethod num-or-str String [s] "It's a string")
#<MultiFn clojure.lang.MultiFn@1774b9b>
user=> (num-or-str 5)
"It's a number"
user=> (num-or-str "5")
"It's a string"

 上の「class」は単なる判定関数。型以外の条件でも分岐できる。

user=> (defmulti fuga #(= % "fuga"))
#'user/fuga
user=> (defmethod fuga true [s] 3)
#<MultiFn clojure.lang.MultiFn@b03be0>
user=> (defmethod fuga false [s] 5)
#<MultiFn clojure.lang.MultiFn@b03be0>
user=> (fuga "fuga")
3
user=> (fuga "hoge")
5

動的スコープ

 Clojureは原則レキシカルスコープだけど、Common Lispのスペシャル変数に相当するのが、スレッドローカルな束縛。

 まず、変数を定義だけしておく。(declare a b)は(def a)(def b)と同じ。

user=> (declare a b)
#'user/b

 次に自由変数を参照している関数を定義する。

user=> (defn hoge [] (+ a b))
#'user/hoge

 この関数をletから呼び出すと、レキシカルスコープなのでエラーになる。

user=> (let [a 3 b 5] (hoge))
java.lang.IllegalStateException: Var user/a is unbound. (NO_SOURCE_FILE:0)

 letと同じ書式でbindngを使うと値が渡る。let同様、bindingの外では使えない。

user=> (binding [a 3 b 5] (hoge))
8

 スレッドローカルな束縛はset!で値を変更できる。あまり推奨されない、最後の手段らしい。

user=> (binding [a 3 b 5] (do (set! a 7)(hoge)))
12

値を共有するためのナニカ

 Clojureでは原則、データは変更不可能(immutable)。たとえば、マップに追加するには、元のマップに値を追加した新しいマップを作ることになる。

user=> (def hoge {:foo 3})
#'user/hoge
user=> (def fuga (assoc hoge :bar 5))
#'user/fuga
user=> hoge
{:foo 3}
user=> fuga
{:bar 5, :foo 3}
user=> (= hoge fuga)
false

 たいていは困らないけど、複数のスレッドなどから同じデータを参照したり操作したりする場合には困る。そういうときにはatomとかrefとかを使う。元のデータを参照するオブジェクトを間にはさんで間接参照し、新しい値をそのatomやrefに登録しなおす感じ。そのatomとかrefとかを共有すれば、同じデータを参照したり操作したりできる。

user=> (def hoge-a (atom {:foo 3, :bar 5}))
#'user/hoge-a
user=> (def hoge-r (ref {:maguro 2, :aji 1}))
#'user/hoge-r

 参照は(deref foo)または@foo。

user=> @hoge-a
{:foo 3, :bar 5}
user=> @hoge-r
{:maguro 2, :aji 1}

 参照先を変更するには、atomではreset!。

user=> (reset! hoge-a (assoc @hoge-a :baz 7))
{:baz 7, :foo 3, :bar 5}
user=> @hoge-a
{:baz 7, :foo 3, :bar 5}

 refではref-set。ただし、refの参照先を変更するにはdosyncでロックをかける必要がある。

user=> (dosync (ref-set hoge-r (assoc @hoge-r :maguro 3)))
{:maguro 3, :aji 1}
user=> @hoge-r
{:maguro 3, :aji 1}

 refのほうが手間は増えるけど、dosyncにより複数のrefを整合性を保って更新できる(STM)。

user=> (def total (ref 400))
#'user/total
user=> (dosync (ref-set hoge-r (assoc @hoge-r :aji 2))(ref-set total 500))
500
user=> @total
500
user=> @hoge-r
{:maguro 3, :aji 2}

コメント

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://emasaka.blog65.fc2.com/tb.php/715-08dc6dde

 | HOME | 

Categories

Recent Entries

Recent Comments

Recent Trackbacks

Appendix

emasaka

emasaka

フリーター。
連絡先はこのへん

Monthly


FC2Ad