Clojureメモ: マクロの動き
マクロの使い方覚え書き。
まず、「プログラミングClojure」に出ていたノーマルな定義。
unlessというマクロ。
(defmacro unless [expr form] (list 'if expr nil form))
実行結果
t-macro.core=> (unless false (println "1")) 1 nil t-macro.core=> (unless true (println "1")) nil
Clojureのマクロは、Cのプリプロセッサと同様に、 定義された内容はいったん展開され、その後式として評価される。
(macroexpand-1)
を使うと展開した後の式が見える。
t-macro.core=> (macroexpand-1 '(unless true (println "1"))) (if true nil (println "1"))
マクロの中身は(list 'if expr nil form)
であるが、
このうちexpr
とform
は引数として渡され、評価されずにそのまま展開される。
一方、(list ...)
は評価されて、これがマクロの結果として返される。
'if
はリーダマクロ'
を使ってifを評価せずにそのまま渡している。
nilは評価されるがnilになる。
ここがポイントのようで、Cのプリプロの単純な文字列置換とはちょっと違う。
マクロを以下のように書き換えてみる。unless-2とする。
(defmacro unless-2 [expr form] (if expr nil form))
さっきとの違いは、(list...)
を外し、'if
ではなくif
を使った。
実行してみる。
t-macro.core=> (unless-2 false (println "1")) 1 nil t-macro.core=> (unless-2 true (println "1")) nil
一見、結果は一緒。
しかし、(macroexpand-1)
すると違いが分かる。
t-macro.core=> (macroexpand '(unless-2 false (print "1"))) (print "1")
マクロとして展開された結果が違う。
今度のはif
がマクロ展開時に実行されたので、その結果がマクロ展開の結果として返っている。
上記を見ると、マクロ定義は簡単に言えば 何かしらのClojureプログラムのリストを返す関数を作ること、と思えばよさそうだ。
とすると、さきほど一見、unless
とunless-2
の実行結果が同じに見えたが、
以下を試してみると、、、
t-macro.core=> (unless (= 1 2) (println "1")) 1 nil t-macro.core=> (unless-2 (= 1 2) (println "1")) nil
結果は違ってくる。 macroexpand-1してみる。
t-macro.core=> (macroexpand-1 '(unless (= 1 2) (println "1"))) (if (= 1 2) nil (println "1")) t-macro.core=> (macroexpand-1 '(unless-2 (= 1 2) (println "1"))) nil
unless-2はなぜnilを返すのだろう?
unless-2はマクロとしては、(if '(= 1 2) nil '(println "1"))
を実行する形になる。
t-macro.core=> (if '(= 1 1) "yes" "no") "yes" t-macro.core=> (if '(= 1 2) "yes" "no") "yes"
(if..)
関数は、評価なしのformをそのまま渡すと true として見なすようだ。
なお、
t-macro.core=> 'true true t-macro.core=> 'fase fase
で、true, falseをquoteしてもtrue, falseになるようだ。 これ忘れるとハマるな。
vimrcメモ(clojure用plugin設定)
vimrcメモ。 Clojureのための設定も追加。
set nocompatible set runtimepath^=~/.vim/bundle/neobundle.vim/ call neobundle#begin(expand('~/.vim/bundle/')) NeoBundleFetch 'Shougo/neobundle.vim' NeoBundle 'guns/vim-clojure-static' NeoBundle 'kien/rainbow_parentheses.vim' call neobundle#end() " Enable file type detection and do language-dependent indenting. filetype plugin indent on NeoBundleCheck " Switch syntax highlighting on syntax on " Show line numbers set number " C indent setting set cindent set cinoptions+=:0 "Change indent rule of 'case' statement " Tab width, no expand to space "set tabstop=4 shiftwidth=4 noexpandtab set tabstop=4 shiftwidth=4 expandtab " Highlight search word, increamental hightlight set hlsearch set incsearch autocmd QuickFixCmdPost [^l]* nested cwindow autocmd QuickFixCmdPost l* nested lwindow " show buffer directly map <C-b> :ls<CR>:buf " rainbow_parentheses.vimの括弧の色付けを有効化 au VimEnter * RainbowParenthesesToggle au Syntax * RainbowParenthesesLoadRound au Syntax * RainbowParenthesesLoadSquare
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)は簡潔すぎるほど、簡潔にかけるな。 定義されている関数の仕様の理解が肝で、油断すると意味不明なプログラムになりそう。