Clojureメモ: loopとrecur
プログラミングClojureに乗っていた例を写経実行したメモ。
関数を使わずに、再帰的にループするやり方。
(println (loop [result [] x 5] (if (zero? x) result (recur (conj result x) (dec x)))))
実行結果:
[5 4 3 2 1]
ループ分の構造としては
(loop [※A] ... ... (recur ※B))
になっていて、※Aでは関数でいう引数の定義と、加え初期値の代入をしている。
[result [] x 5]
の意味は、result = []
、x = 5
。
(recur ※B)
では、loopの先頭に戻って、※Bで次のresult
とx
を与えている。
これは、イメージ的には無名関数の再帰呼び出しような感じだなあ。と。
関数でrecurを使う場合は以下のようになる。
(defn count-down [result x] (if (zero? x) result (recur (conj result x) (dec x)))) (println (count-down [] 5))
(loop [result [] x 5]
を(defn count-down [result x]
に置き換えただけだ。
初期値は呼び出し側が与えるので、関数の場合は引数の初期値ない。
関数のrecurを使う再帰呼び出し、loopのrecurの記述の一貫性はビューティフルだな。
Clojureメモ: 無名関数
「プログラミングClojure」で出てきたサンプル。
user=> (filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+")) ("fine" "day")
filter関数は、
(filter pred coll)
コレクションcollの各要素に対して、条件をチェックする関数predを適応して、trueが返ってきたものだけを、
戻りのシーケンスに入れて返す。
rubyでいうとArray#select
に相当する。
さて、上記の(fn [w] (> (count w) 2))
が無名関数で、リーダマクロ#()
を使って、#(> (count %) 2)
という形で書ける。
user=> (filter #(> (count %) 2) (str/split "A fine day" #"\W+")) ("fine" "day")
無名関数は任意の変数にバインドできる。 (Clojureのvarは変数とは厳密に違うようだが)
a
にバインドして実行してみる。
user=> (def a #(> (count %) 2)) #'user/a
user=> (a "abc") true user=> (a "jk") false
ここら辺の関数の扱いの気軽さはjavascript並みだ。 javascriptで書くと下記のようなものになるかと。
var a = function(w) { return (count(w)>2); } a("abc);
最後に冒頭の例で、a
にバインドされた無名関数を使ってみる。
user=> (filter a (str/split "A fine day" #"\W+")) ("fine" "day")
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のクラスのようだ。