Programmer's Note

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

Clojureのプログラミング本とか

先週あたり、ひととおり「プログラミングClojure」を読み終えた。

この本は正直、分かりやすくない。 用語が統一されてない部分があったり、用語の説明がいまいち足りなく、 ネットで検索せんといかんかったり。

サンプルコードも面白い訳でもなく、かといってリファレンスとしても使えないし、 とまあ結構さんざんな言いようだが・・・原著はRich Hickeyさんが ブックリスト に追加しているくらいだから、結構評価されている本なんでしょうね。 (自分には合わんかったというだけかな)

さて、もうちょっとLispのプログラミングを知ろうと思って、 SICP を読み始めた。英語のペーパーバック。 (以前、しょっぱなの数ページだけ読んで止めていた)

一方で、たまたまオンラインでフリーで読める本を見つけた。 Learn to Program the World's Most Bodacious Language with Clojure for the Brave and Trueは、なかなか面白そうだ。

おちゃらけた感じで書かれてるが、こーいうのは好きだな。 (Joel on Softwareもそんな感じだしな。) でもしょっぱなからClojureのクールなところを感じさせる部分を紹介していたり、 なかなかセンスよさげ。これもあとで紙本を買って読みたいところだ。

Clojureメモ: 再帰を使うとか使わないとか

再帰呼び出し。 練習がてら下記のような関数を作ってみる。

(myfunc 5 [])
=> [1 2 3 4 5]

考え方としては、

(myfunc 5, [])
  -> (myfunc 4, [5])
     -> (myfunc 3, [4 5])
        -> (myfunc 2, [3 4 5])
           ...
           -> (myfunc 0, [1 2 3 4 5])
                => 最後に[1 2 3 4 5]を返す

実装すると以下のようになる。

(defn myfunc [n coll]
  (if (<= n 0)
    coll
    (myfunc (dec n) (cons n coll))))
t-recur-func.core=> (myfunc 5 [])
(1 2 3 4 5)

これは、引数にコレクション(ベクター)を渡してしまうが、 コレクションを渡さない以下のやり方も考えられる。

(defn myfunc2 [n]
  (if (<= n 1)
    [n]
    (conj (myfunc2 (dec n)) n)))
t-recur-func.core=> (myfunc2 5)
[1 2 3 4 5]

こっちは、呼び出した関数の返り値を、どんどんくっつけていくパターン。

どちらかというと、個人的にはこちらの方が実装のイメージがしやすかった。

ただし、こちらの方が末尾再帰ではないので、(recur ..)に置き換えることができず最適化できない。 (再帰呼び出しの回数が多いとスタックオーバーフローを起こす)

とはいえ、このパターンでフィボナッチ数列を求めることもできる。

(defn fib [a b n]
    (if (<= n 1)
      [a] 
      (cons a (fib b (+ a b) (dec n)))))
t-recur-func.core=> (fib 0 1 7)
(0 1 1 2 3 5 8)

てな感じだが、 最後の一行がごちゃごちゃしてて、あまり分かりやすくない…。

フィボナッチ数列の求め方は、Clojure的にはやはり、再帰を使わないやり方がエレガント。 以下のような感じ。

(defn fib2 [n]
  (letfn [(_fb [[a b]]
            [b (+ a b)])]
    (take n
        (map first (iterate _fb [0 1])))))
t-recur-func.core=> (fib2 8)
(0 1 1 2 3 5 8 13)

これは「プログラミングClojure」に載っていた解法。 (ポイントを思い出して書いたので、そのままではない)

一旦、

([0 1] [1 1] [1 2] [2 3] [3 5]...)

のシーケンスを求めといて、 それぞれの最初の要素を取り出してしまう。という考え方。

まあ、mapとかiterateとかの仕様を知らないと読めないのだが。 でも、Clojureレバレッジを最大に生かすこの解法には膝を打ったな。

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回目はできあいのシーケンスをそのまま使うてことだな。