2012-02-29

末尾再帰で木を走査

何年か前、Scheme:末尾再帰で木をトラバースを読んだときは何をしてるかさっぱりだったんだけど、今なら分かるんじゃないかなー、と思って暇な時間に考えてたら自分でも書けたので、結果に至る過程を記録。

ちょっとアレンジして、木のコピーを例に考える。まずは、普通の再帰で素直に書いてみる。

;; 素朴な木のコピー
(define (copy-tree tree)
  (let loop ((node tree))
    (if (pair? node)
        (cons (loop (car node)) (loop (cdr node)))
        node)))

単純で分かりやすい。これをベースに、継続渡しスタイルを利用して末尾再帰のコードに変換してみる。人力CPS変換。

ちなみに、継続渡しスタイルについての詳しい話や、継続渡しスタイルと末尾再帰(末尾呼び出し)の関係についての説明は、ここではしない。他人に色々説明できるほど深く理解をしているわけじゃないので。詳しく知りたい人は、先人の書いた良い文章があると思うから、そちらを読んで欲しい。例えばThe 90 minute Scheme to C compilerとか。

とりあえず、最初に最終的なコードから。

;; 継続渡しスタイルによる末尾再帰な木のコピー
(define (copy-tree tree)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (loop (car node)
              (lambda (x)
                (loop (cdr node)
                      (lambda (y)
                        (cont (cons x y))))))
        (cont node))))

再帰に馴染みがないと、見た瞬間に心理的に3メートルくらい引くと思う。今あらためて見たら、書いた自分でも1メートルくらい引いた。だけど、段階を踏めばそんなに意味不明でもないので、あまり心配しなくても良い。

最初の素直な再帰の例に戻って、

;; 素朴な木のコピー
(define (copy-tree tree)
  (let loop ((node tree))
    (if (pair? node)
        (cons (loop (car node)) (loop (cdr node)))
        node)))

まず、継続を渡していくための引数が必要になる。ここでは、contという引数を増やす。なお、継続に定番のkとかいう名前を代わりに付けると、そっち方面への馴染みが薄い人への攻撃力をさらに強化できる。継続の中身はまだ考えない。

;; 継続渡しのために引数を増やす
(define (copy-tree tree)
  (let loop ((node tree)
             (cont ))
    (if (pair? node)
        (cons (loop (car node)) (loop (cdr node)))
        node)))

次に、最初にcontに指定する継続を考える。つまり、コピーし終わった木を受け取る、一番最後に実行される継続。copy-treeはコピーした木を返す手続きにしたいので、受け取った値をそのまま返す継続が良い。(lambda (x) x)とかでも良いけど、出来合いで、より省タイプなvaluesを使う。最近はCommon Lispに染まっていたのでidentityを探したが、なかった。

;; 最初に指定する継続はvalues
(define (copy-tree tree)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (cons (loop (car node)) (loop (cdr node)))
        node)))

継続渡しスタイルでは、値を返す代わりに、値を渡して継続を呼ぶので、値を返す部分はすべて継続の呼び出しになる。この例では二ヶ所。

;; 値を返す部分で継続を呼ぶ
(define (copy-tree tree)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (cont (cons (loop (car node)) (loop (cdr node))))
        (cont node))))

そして本題。loopの再帰呼び出しのときに指定する継続について考える。まずは(loop (car node))から。

(loop (car node))の継続、つまり、(loop (car node))が返す値をどうしたいのかという話だけど、ご覧の通り、(loop (cdr node))が返す値とペアを作りたい。そして、それを継続contに渡したい。コードで表現するとこうなる。

;; (loop (car node))の継続
(lambda (x)
  (cont (cons x (loop (cdr node)))))

継続が分かったので、実際のコードに当てはめてみる。

;; (loop (car node))を継続渡しスタイルに変更
(define (copy-tree tree)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (loop (car node)
              (lambda (x)
                (cont (cons x (loop (cdr node))))))
        (cont node))))

同じように、次は(loop (cdr node))の継続についても考えてみる。xとペアを作り、contに渡したいので、

;; (loop (cdr node))の継続
(lambda (y)
  (cont (cons x y)))

となる。これを実際のコードに当てはめると、

;; (loop (cdr node))を継続渡しスタイルに変更
(define (copy-tree tree)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (loop (car node)
              (lambda (x)
                (loop (cdr node)
                      (lambda (y)
                        (cont (cons x y))))))
        (cont node))))

最初に紹介したコードと同じになった。ご覧の通り、二ヶ所ある再帰は両方とも末尾呼び出しになっている。

実際に試してみると、

gosh> (let* ((t1 '(0 1 2))
             (t2 (copy-tree t1)))
        (values (eq? t1 t2) (equal? t1 t2) t2))
#f
#t
(0 1 2)
gosh> (let* ((t1 '(0 (1 2)))
             (t2 (copy-tree t1)))
        (values (eq? t1 t2) (equal? t1 t2) t2))
#f
#t
(0 (1 2))
gosh> (let* ((t1 '(0 ((1) 2) 3))
             (t2 (copy-tree t1)))
        (values (eq? t1 t2) (equal? t1 t2) t2))
#f
#t
(0 ((1) 2) 3)
gosh> 

意図した通りの動作をしてる模様。めでたし。

以下おまけ。Common Lispに(中略)ので、constantlyを探したがなかったため、(lambda _ 1)

;; 共通のwalkerを定義
(define (tree-walk tree leaf-proc inner-proc)
  (let loop ((node tree)
             (cont values))
    (if (pair? node)
        (loop (car node)
              (lambda (x)
                (loop (cdr node)
                      (lambda (y)
                        (cont (inner-proc x y))))))
        (cont (leaf-proc node)))))

;; 木のコピー
(define (copy-tree tree)
  (tree-walk tree values cons))

;; 葉を数える
(define (count-leaf tree)
  (tree-walk tree (lambda _ 1) +))

;; 葉を文字列に変換した木を作る
(define (copy-tree/string-node tree)
  (tree-walk tree
             (lambda (x)
               (if (null? x) x (x->string x)))
             cons))

関数型便利。

2011-11-28

LevelDBの共有ライブラリを作る

2011年11月28日現在、LevelDBのMakefileはまだ共有ライブラリの生成に対応してないので、それについてのworkaround。何度も同じことを考えるのを防ぐための備忘用。

まず、元になるオブジェクトコードを作る。

CFLAGS="-I$HOME/opt/snappy-1.0.4/include" \
gmake CC="g++ -m64" OPT="-O2 -DNDEBUG -fPIC" \
SNAPPY_CFLAGS="-I$HOME/opt/snappy-1.0.4/include -DSNAPPY"          

各指定の簡単な解説。

最初のCFLAGSはSnappyのヘッダの位置をプリプロセッサに伝える。標準の位置にあるならこの指定は不要。build_detect_platformでのみ有効で、MakefileでCFLAGSは上書きされる。SNAPPY_CFLAGSでも同じ指定をしているのはそのため。

今回は64ビットのライブラリを作りたい(Clozure CLの64ビットバイナリからFFI経由で使いたくなった)ので、CCはg++ -m64で。GCC 4.7より前のバージョンでは、GCCをビルドするときに設定しない限り、Solaris/x86-64でもデフォルトで64ビットコードを作らない。Solaris 11の標準のGCC 4は4.5.2。

効率的に動作して欲しいので、-fPICを付ける。適当な場所が見当たらなかったので、OPTに追加。

SNAPPY_CFLAGSは上に書いているように、ヘッダの位置の指定を追加。

次に、共有ライブラリを作る。

g++ -m64 -shared -Wl,-h,libleveldb.so.0 \
db/*.o port/*.o table/*.o util/*.o \
-Wl,-R,$HOME/opt/lib -L$HOME/opt/snappy-1.0.4/lib \
-lsnappy -lpthread -lrt -o libleveldb.so.0.0

64ビットのライブラリを作るため、-m64を指定するのは同じ。

-sharedで共有ライブラリを作ると指定し、-Wl,-hでsonameを指定するためのオプションをリンカに渡す。Solarisのldでは-sonameではなく-h。あるいは--soname。

材料のオブジェクトファイルは上の例のように指定すればすべて揃う。不安ならmakeしたときにarに渡されているファイルと照らし合わせれば確認できる。

リンクするライブラリは、Snappyと、環境に依存するライブラリ。これは、makeするときにできるbuild_config.mkを見れば分かる。PLATFORM_LDFLAGSに指定されているもの。Snappyも64ビットとしてビルドされていないと、当然リンクできないので注意。Snappyが標準の場所にないので、-Wl,-RでRUNPATHも指定する。

以上。あとは適当にサーチパスに放り込んで、libleveldb.so.0とlibleveldb.soという名前のシンボリックリンクをlibleveldb.so.0.0に張れば終わり。

2011-08-31

16進ダンプ

REPLから使いたくなって書いた。

使い方は以下の通り。(unsigned-byte 8)を要素に持つシーケンスを扱える。十行ごとにヘッダが入り、アドレス表示部分は長さ(標準で8桁)とオフセットを指定できる。テキスト表示部分はstandard characterかつgraphicな文字だけ表示できる。

> (hex-dump (sb-ext:string-to-octets "string"))
========== +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F =================
00000000 : 73 74 72 69 6E 67                               | string
NIL
>

2011-08-05

cl-fn

昨日のdefaliasだけど、似たようなコードを発見した。

Ron Garretユーティリティライブラリで、

(defmacro define-synonym (s1 s2)
  `(progn
     (defun ,s1 (&rest args) (declare (ignore args)))
     (setf (symbol-function ',s1) (function ,s2))))

という関数を定義してた。

何度か目を通してるはずなのに何故気付かなかったんだ、と一瞬後悔したけど、良く見たらs2を直接function特殊形式に渡してる。これだと、関数名lambda式以外を渡すことが許されないので、純粋に別名を付けるためだけを目的としたもののようだ。セーフ。こちらの主な目的は関数合成とかの結果に名前を付けることだから、上のコードは使えない。

安心した所で、無駄にならなかったdefaliasを、関数を扱うときに良く使われるユーティリティ関数とまとめてcl-fnというライブラリにした。ついでに、lambdaを書くのに疲れてきたので、前述のRon Garretのユーティリティからfnというマクロも貰ってきた。

;; &restを使わずに引数全体を束縛できる
(funcall (fn args args) 0 1 2)
;=> (0 1 2)

;; _で引数を無視できる(警告が出ないように宣言も付く)
(funcall (fn (x _) x) 0 1)
;=> 0

なんてことができるちょっと賢いやつ。ただし、リーダには手を入れないので、

((fn (x) x) 0)

みたいなことはできない。直接lambda式を呼びたいことなんてあんまりないから問題ないとは思うけど。

ちなみに、使いたい機能だけ楽にインポートできるように、パッケージを細かく分けておいた。例えば、curryとrcurryだけ使いたい場合は、cl-fn.paパッケージだけ使えば大丈夫。全部入りが欲しいならcl-fnを使う。余分なものまでインポートしなくて良いので綺麗好きな人でも安心。

2011-08-04

defalias

Emacs Lispには、関数に別名を付けるdefaliasという関数があるんだけど、これが欲しくなったので書いてみた。

(defmacro defalias (name function-designator)
  (with-gensyms (function designator)
    `(let* ((,designator ,function-designator)
            (,function (if (functionp ,designator)
                           ,designator
                           (symbol-function ,designator))))
       (setf (symbol-function ',name) ,function)
       ',name)))

マクロにしたのはクォートを付けるのが面倒だから。第二引数はfunction designatorなので、シンボルでも関数でも大丈夫。

普通に別名を付けることもできるけど、考えている主な使い所は、合成したり部分適用した関数に名前を付けたりするとき。

;; これを
(setf (symbol-function '2*)
      (curry #'* 2))

;; こう書ける
(defalias 2* (curry #'* 2))

(2* 3)  ;=> 6

他にも誰かが似たようなものを作ってそうだったから探してみたんだけど、見付けられなかった。