Programmer's Note

コード読み書きの備忘録。

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)であるが、 このうちexprformは引数として渡され、評価されずにそのまま展開される。

一方、(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プログラムのリストを返す関数を作ること、と思えばよさそうだ。

とすると、さきほど一見、unlessunless-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で次のresultxを与えている。

これは、イメージ的には無名関数の再帰呼び出しような感じだなあ。と。

関数で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とかの感覚で書いていると、まったくよくわからない。

で、いろいろ試してみて、結局、以下のように理解した。

  1. まず、上記(for ...)では2要素のシーケンス(nil, nil)を返す。
  2. 返り値の中身がnilなのは、(for ...)の中の実行関数(print x)nilを返すから
  3. REPLの場合は、REPLの環境自体がそのシーケンスを(表示するために)使ったので、その場で評価された。
  4. 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の場合と結果が違うのが気になる。こちらは()の中に12が表示されている。 これ、ぎりぎりまでシーケンス生成((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)は簡潔すぎるほど、簡潔にかけるな。 定義されている関数の仕様の理解が肝で、油断すると意味不明なプログラムになりそう。