Clojureメモ: 遅延評価
さて、(for ...)
はシーケンスを生成して返すが、これは遅延評価されるので、実際に使用されるまではシーケンスは生成されない。
REPL環境だと入力した式がその場で評価されるので、これが遅延評価されるものとは気づかない。
user=> (for [x (range 1 3)] (println x)) 1 2 (nil nil)
実際に、lein new app sample
とかでテンプレートつくって、以下のようなソースを書いてみる。
(ns sample.core (:gen-class)) (defn -main "I don't do a whole lot ... yet." [& args] (for [x (range 1 3)] (println x)))
実行してみると
sample $lein run sample $
、、、なにも表示されない?
CとかRubyとかの感覚で書いていると、まったくよくわからない。
で、いろいろ試してみて、結局、以下のように理解した。
- まず、上記
(for ...)
では2要素のシーケンス(nil, nil)
を返す。 - 返り値の中身がnilなのは、
(for ...)
の中の実行関数(print x)
がnilを返すから - REPLの場合は、REPLの環境自体がそのシーケンスを(表示するために)使ったので、その場で評価された。
lein run
で実行した場合は、プログラムのなかで(for ...)
の返り値のシーケンスを使うコードがどこにもないので、評価されずに終わった
ということか。
プログラムを下記のように書き直す。
(ns sample.core (:gen-class)) (defn -main "I don't do a whole lot ... yet." [& args] (println (for [x (range 1 3)] (println x))))
実行結果:
sample $lein run (1 2 nil nil)
微妙にREPLの場合と結果が違うのが気になる。こちらは()
の中に1
、2
が表示されている。
これ、ぎりぎりまでシーケンス生成((for..)
の中の実行)を遅らせているのだな、と理解した。
遅延評価は一回しかやらないようで、例えば、
(ns sample.core (:gen-class)) (defn -main "I don't do a whole lot ... yet." [& args] (def a (for [x (range 1 3)] (println x))) (println a) (println a))
の実行結果は、
sample $lein run (1 2 nil nil) (nil nil)
になる。
2回目はできあいのシーケンスをそのまま使うてことだな。
Clojureを遊ぶ:括弧とか
「プログラミングClojure」で出てきたforループのサンプル。
(defn indexed [coll] (map vector (iterate inc 0) coll)) (defn index-filter [pred coll] (when pred (for [[idx elt] (indexed coll) :when (pred elt)] idx)))
実行結果:
(index-filter #{\a} "dfajklfabce") -> (2 7)
初学者の自分がこれを読み解くのに苦労した。
お遊びで、Rubyっぽく表記を変えてみる。
def indexed (coll) map(vector, iterate(inc, 0), coll) end def index-filter (pred, coll) when pred for (idx, elt) in indexed(coll) :when pred(elt) idx end end end
こういうお遊びは楽しい。
括弧の()
や[]
という表記は、範囲を表すためのものだが、
ネストすると脳内で解読が必要になってしまう。
元のコードだと[[idx elt] ...]
あたりで目がちかちかするW。
処理によって表現に使う記号を変えるだけで、ずいぶんと読みやすくなる。
一方で、Paul Grahamがどっかの本で書いてあったのだが、 LISPer人たちは、括弧は無視してて、エディタの力を借りて、 インデントで構文を読んでいると。(最近はシンタックスハイライトもあるしね)
元のコードから、人間には解読可能な範囲で、括弧をなくすだけでも、ずいぶんと読みやすくなる。
defn index-filter [pred coll] when pred for [idx elt] (indexed coll) :when (pred elt) idx
こうするとPythonっぽいよな。
Clojureメモ:forを使う(その2)
まずは、2次元配列的なものを作ってみる。
user=> (def aa [[1 \a] [2 \b]]) #'user/aa user=> aa [[1 \a] [2 \b]]
これを、一個ずつ取り出してみる。
user=> (for [x aa] (println x)) [1 a] [2 b] (nil nil)
で、上記[1 a]
、[2 b]
の第1要素だけをとりたい。
C言語とかはこういうときは2重ループでやるしかないが、Clojureは非常にシンプルに記述できる。
user=> (for [[x y] aa] (println x)) 1 2 (nil nil)
便利!
Clojureのdestructingの機能を使っていて、[[x y] aa]
は、Rubyのタプルみたいなものでx, y = aa
だ。
2重ループもスーパーシンプル。
user=> (for [i [1 2] j [3 4]] [i j]) ([1 3] [1 4] [2 3] [2 4])
一行だと分かりにくいので、改行してインデントしてみる。
(for [i [1 2] j [3 4]] [i j])
これRuby的に書くと
ret = [] for i in [1,2] for j in [3,4] ret.push([i,j]) end end ret
Clojure (LISP)は簡潔すぎるほど、簡潔にかけるな。 定義されている関数の仕様の理解が肝で、油断すると意味不明なプログラムになりそう。
Clojureメモ:forを使う
Clojureを始めて1週間ちょいといったところか。 とりあえず本を読んでいる、ちょくちょく試してみている感じ。
ループのまわし方とかも、手続き型プログラミングと勝手が違って戸惑う。
※Clojureのforは命令型言語のforループとは違うらしい。(リスト内包表記と呼ばれる。)
forの一番シンプルな例。
user=> (for [i [0 1 2 3]] (println i)) 0 1 2 3 (nil nil nil nil)
最後の(nil ...)
は関数の返り値をREPL環境が表示している。
(for ...)
は制御文ではなく関数なので戻り値がある。
(for ...)
はつまり、ループ分だけ要素があるコレクションを作って返すということか。
さて、ループをまわすには、要はまず何かの集合が必要だということだな。
上記の例だと、集合[0 1 2 3]
の中から一個ずつアイテムを取り出してi
に代入し、それを使って処理している。
(Clojureの用語だとシンボルi
に値をバインドする、と言う。)
上記はClojureのベクター(要は配列)を使ったが、次にマップを使ってみる。
user=> (def a {:m1 1 :m2 2}) #'user/a
user=> a {:m1 1, :m2 2}
user=> (for [i a] (println i)) [:m1 1] [:m2 2] (nil nil)
おお、期待通りの動作。マップの順番って定義された順だろうか・・・まあ、あとで調べるとしよう。
1から10の数字を表示する関数を書いてみる。
1〜10をまわすが、[1 2 ...]
で定義するのも面倒で、range
関数を使う。
user=> (range 1 11) (1 2 3 4 5 6 7 8 9 10)
でとりあえず、
user=> (for [i (range 1 11)] (println i)) 1 2 3 4 5 6 7 8 9 10 (nil nil nil nil nil nil nil nil nil nil)
てな感じだ。
Clojureメモ:文字列分割
「プログラミングClojure」を読んでClojureを学んでいる。 正直、この本は入門者向けではない…。著者の気持ちが先走りすぎている。 いやいや、面白いのは分かるけど、もうちょっと落ち着いて教えてくれよ。という感じだ。
まあ謎解きしながら読むのも面白いが…。
軽く文字列処理のメモ。 文字列をスペースとかで分割する例。
user=> (require '[clojure.string :as str]) nil user=> (str/split "This is a pen." #"\W+") ["This" "is" "a" "pen"]
#"\W+"
は正規表現で英数字以外の文字を表す。
(正規表現はリーダマクロ表記の#""
で囲む)
正規表現もいろいろ方言があるが、Clojureのは標準的なのだろうか。 軽くググっても出てこんかった。 本家のHPからhttp://www.regular-expressions.info/がたどれた。 あとで調べてみよう。
user=> (class #"") java.util.regex.Pattern
とすると下の層ではjavaのクラスのようだ。
Clojureを遊ぶ:リストとマクロ
「7つの言語7つの世界」 を読んでClojureを始めた。
まったく思考がLISPerになってなく、すらすらプログラムが組める状態ではない。が、実に面白い。
Clojureの強力な点は、コレクションに対する操作を、非常に簡潔に書けるところだ。
まずはリスト。
リストは(list 1 2 3)
と書く。あるいはリーダーマクロの'
を使って'(1 2 3)
と書く。
user=> (def a '(1 2 3)) #'user/a user=> a (1 2 3) user=> (take 2 a) (1 2)
LISPのプログラムそのものがリストで構成されているデータで、
user=> (def b '(println 1)) #'user/b user=> b (println 1) user=> (eval b) 1 nil user=>
なんか、これだけでミニマクロみたいなものだな。
本当のマクロを使ってみる。
user=> (defmacro hello [txt] (println "Hello," txt)) #'user/hello user=> (hello "Clojure") Hello, Clojure nil
これくらいなら、普通に関数を定義すればよさそうだ。 実際やってみる。
user=> (defn hello_f [txt] (println "Hello," txt)) #'user/hello_f user=> (hello "Clojure") Hello, Clojure nil
同じ結果が出る。
しかし、以下で明確に処理の違いが分かる。
user=> (hello (print 1)) Hello, (print 1) nil user=> (hello_f (print 1)) 1Hello, nil nil
マクロの場合は引数を評価せずに、そのままリストとして渡すのに対して、 関数の場合は引数を評価してから渡す。
というところまでは分かったが、まだまだ感覚をつかめていない。
マコネル本の読書
遅ればせながら、最近やっとCode Completeを読んだ。まだ上巻を読了したばかりだが、この本がこんなに面白いとは思わなかった。
ずいぶん前に買って積んでいたが、言わば食わず嫌いだったな。なぜかって一言で言えばMicrosoft Pressだから。
この本は名著ともっぱら評判は良いみたいだけど、MSの人が書いたっぽいからつまらないに違いない。と思い込んでた。反省。
MSの製品はつまらないが、必ずしも出す本はつまらない訳じゃないのね。
(著者のSteve McConnelさんは、厳密にはMSのプロジェクトに関わってたが社員じゃなかったようだ)
元MSの人でやはり面白い本を出してるJoelさんの例もあって、最近はすっかりMSに対するイメージがアップしている。
さて、McConnelさんの他の著作で、エッセイ的なProfessional Software Developmentを今読んでいるが、これも面白い。この人は重厚に文献を読んで分かりやすく本質的な問題を分析する人だなと。
とりあえず前半で「ソフトウェアはソフトではない(変更が簡単だと思うど素人の多いこと)」「銀の弾丸は失敗する(組織がプロセス改善の流行に流されたらおしまいよ)」。
まあ原文ままではないが、そんなことを言っていて、読んでため息し膝を打つのだった。いやまさに、あるある。だなあと。