時には実用を考えずにマクロを書いて遊んでいると楽しいというだけの記事です。

Go言語の defer が簡単に実装できそうな気がしたので書いてみました。と思っていたら、途中で記事後半の事実に気付いて思ったよりも長くなりました。

defer の単純な模倣

Go言語における defer の動作

Go言語において、 defer を利用することで、関数スコープを抜けるときに実行する動作を記述することができます。リソースのクローズなどに利用しますが、ここではそうした実践的な例は捨ておき、次のような例のための例を見ます（以下は適宜抜粋したもの。動作するコードは https://play.golang.org/p/EWOSf5oa3_5 ）

func test(x bool ) { fmt.Println( "before" ) defer fmt.Println( "after" ) if !x { return } defer func () { fmt.Println( "1" ) fmt.Println( "2" ) }() fmt.Println( "3" ) }

この defer には次のような特徴があります。

複数の defer がある場合、通ったのとは逆順で実行される 実行時に通った defer のみが動作する

まず特徴1を見るため、 test(true) として実行すると次のような出力になります。後ろにある "1", "2" を出力する defer の方が、"after" を出力する defer よりも先に実行されています。

before 3 1 2 after

次に特徴2を見るため、 test(false) として実行すると次のような出力になります。途中で return して通過しなかった "1", "2" を出力する defer は実行されません。

before after

マクロを書いて模倣する: with-defer

上で見たような特徴を持つ defer を利用することができるマクロ with-defer を作ってみます。

実装は後で見ますが、これを利用することで先程の test 関数と同じ動作の関数を次のように書けます。ただし、 defer が実行されるのは関数スコープを抜けるときではなく、 with-defer を抜けるときになります*1。

( defun test ( x ) ( with-defer ( print ' before ) ( defer ( print ' after )) ( unless x ( return-from test )) ( defer ( print 1 ) ( print 2 )) ( print 3 )))

動かしてみます。まずは (test t) です。下記の通り、後に出てくる defer （"1", "2" を出力）が最初の defer （"AFTER" を出力）よりも先に実行されます。

BEFORE 3 1 2 AFTER

次に (test nil) です。下記の通り、到達しなかった2つ目の defer は実行されません。

BEFORE AFTER

さて肝心の実装ですが、下記の通り10行程度のマクロで実現できます。

( defmacro with-defer ( &body body ) ( let (( g-defer-list ( gensym "DEFER-LIST" )) ( g-body ( gensym "BODY" ))) `( let ( ,g-defer-list ) ( macrolet (( defer ( &body ,g-body ) `( push ( lambda () ,@,g-body ) , ' ,g-defer-list ) )) ( unwind-protect ( progn ,@body ) ( dolist ( func ,g-defer-list ) ( funcall func )))) ) ))

with-defer の中でのみ動作するローカルマクロ defer は、渡された g-body 部で無名関数を組み上げ、リスト g-defer-list に追加します。このリストに詰め込まれた無名関数は unwind-protect を抜けるとき（いわゆる try-finally の fianlly 部）に、リストに入れたのとは逆の順序で取り出されて実行されます。

という感じで、容易に動作を模倣することができました。

ちなみに、複数行に渡る動作を defer に渡す場合、Go言語では次のように明示的に無名関数で包んであげる必要があります。

defer func () { fmt.Println( "1" ) fmt.Println( "2" ) }()

一方で、上で実装した defer は暗黙的に無名関数で包んでいるため、複数行に渡る動作も単に並べるだけで良いです。

( defer ( print 1 ) ( print 2 ))

deferの直接呼び出し形式における引数の先行評価

Go言語の場合

実は、上記で実装した defer では1つ模倣できていないことがあります。それは、直接呼び出し形式における引数の先行評価です...というと分かりにくいですが、例を見てみます（ https://play.golang.org/p/HwOBMnxEe33 ）。

func test() { for i := 0 ; i < 3 ; i++ { defer fmt.Printf( "direct: %d

" , i) } for i := 0 ; i < 3 ; i++ { defer func () { fmt.Printf( "closure: %d

" , i) }() } }

この test 関数を実行すると次のような結果になります*2。見ての通り、クロージャに包んだ方は、ループが回りきった後の i の値である3を出力し、直接呼び出した方は defer が評価された時点の i の値を出力します。

closure: 3 closure: 3 closure: 3 direct: 2 direct: 1 direct: 0

これは、直接呼び出しの場合に限り、引数をその場で評価することから来る動作のようです。次のようにすると、その動作がよりはっきりします（ https://play.golang.org/p/z_m-Kr-pxsM ）。

func f(s string ) string { fmt.Println(s) return "inner" } func test() { defer f(f( "outer" )) fmt.Println( "normal" ) }

この test 関数を実行すると次のような結果になります。 defer に渡した f(f("outer")) の「引数」である f("outer") は defer を通過した時点で評価されます。このため、"normal" よりも先に "outer" の出力が来ることになります。

outer normal inner

引数の先行評価を模倣する: with-defer2

先程作成した with-defer マクロ内の defer は単にクロージャで包んでいるだけであるため、上で見たような先行評価を模倣することができていません。

( with-defer ( dotimes ( i 3 ) ( defer ( print i ))))

上記を実行すると、ループ後の i の値3が取り出されています。

3 3 3

さて、引数の先行評価を模倣するにあたり、まずは実現方式を考えてみます。現在の with-defer を手で展開すると次のようになります。

( let ( lst ) ( unwind-protect ( dotimes ( i 3 ) ( push ( lambda () ( print i )) lst )) ( dolist ( fn lst ) ( funcall fn ))))

次のようにすることで、 defer = push 時点で i を評価させることができます。

( let ( lst ) ( unwind-protect ( dotimes ( i 3 ) ( push ( let (( x i )) ( lambda () ( print x ))) lst )) ( dolist ( fn lst ) ( funcall fn ))))

上記の出力は目的通りの形になります。

2 1 0

後はこれをマクロとして実装するだけです。その前の準備として、ネストしたバッククォートを扱い続けるのはしんどいので、補助関数を切り出します。先程の with-defer と等価な実装は下記の通りです。

( defun defer% ( defer-list body ) `( push ( lambda () ,@body ) ,defer-list ) ) ( defmacro with-defer ( &body body ) ( let (( g-defer-list ( gensym "DEFER-LIST" )) ( g-body ( gensym "BODY" ))) `( let ( ,g-defer-list ) ( macrolet (( defer ( &body ,g-body ) ( defer% ' ,g-defer-list ,g-body ))) ( unwind-protect ( progn ,@body ) ( dolist ( func ,g-defer-list ) ( funcall func )))) ) ))

できあがったものがこちらです。行数としては30行程度ですが、だいぶしんどい実装になっています。しんどい主な原因はGo言語の defer における「直接呼び出しで書けるのは関数呼び出しに限る」という制約がないためです。 with-defer の場合、アトム, 関数, マクロ, スペシャルフォームのいずれも許容していますが、このうち単純に引数を評価してよいのは関数だけです。この辺りの振り分けをしているのが true-function-p です（たぶんバグがあります）。

( defun true-function-p ( head env ) ( if ( listp head ) ( functionp head ) ( and ( fboundp head ) ( not ( special-operator-p head )) ( not ( macro-function head env ))))) ( defun defer2% ( defer-list body env ) `( push , ( if ( and ( = ( length body ) 1 ) ( listp ( car body )) ( true-function-p ( caar body ) env )) ( let (( args ( loop :for i :from 0 :below ( length ( cdar body )) :collect ( intern ( format nil "A~D" i ))))) `( let , ( loop :for arg :in args :for exp :in ( cdar body ) :collect ( list arg exp )) ( lambda () ( , ( caar body ) ,@args )) ) ) `( lambda () ,@body ) ) ,defer-list ) ) ( defmacro with-defer2 ( &body body &environment env ) ( let (( g-defer-list ( gensym "DEFER-LIST" )) ( g-body ( gensym "BODY" ))) `( let ( ,g-defer-list ) ( macrolet (( defer ( &body ,g-body ) ( defer2% ' ,g-defer-list ,g-body ,env ))) ( unwind-protect ( progn ,@body ) ( dolist ( func ,g-defer-list ) ( funcall func )))) ) ))

ということで、下記のように呼び出してみると...

( with-defer2 ( dotimes ( i 3 ) ( defer ( print i ))))

所望の通り、先行評価できていることが分かります。

2 1 0

しかし、せっかく関数以外も受け入れられたり、複数行（= body部が2つ以上）を受け入れられたりするのに、次の例に見るように色々な制約があって今いちしっくりきません。

( with-defer2 ( dotimes ( i 3 ) ( defer ( let () ( print i ))))) ( with-defer2 ( dotimes ( i 3 ) ( defer ( print i ) ( print i ))))

関数以外の場合に、必要な部分だけ正しく先行評価をするのは結構大変です。複数行に渡って先行評価を行うようにすることはまだ容易ですが、関数以外への対応をしないまま導入しても、式によって先行評価されたりされなかったりとなって余り嬉しくはなさそうです。

別の道を探ってみる: with-defer!

さて、前節で実装した with-defer2 ですが、動作はGo言語を模倣しているものの今いちしっくりこない結果となってしまいました。

ここで、マクロ側に自動で先行評価の有無を判断させるという方向性を捨てて、マクロの利用者に先行評価をコントロールさせる道を考えてみます。Common Lisp は多大な自由を与える代わりにプログラマにその制御の責を負わせる傾向が強い言語であるため、こうした方向の方が馴染みそうです。

ということで、次のようにプレフィックス c! をつけたシンボルは先行評価された元の（=プレフィックスなしの）シンボルの値が入るような with-defer! マクロを書くことにします。

( with-defer! ( dotimes ( i 3 ) ( defer ( format t "~D:~D~%" c!i i ))))

実行結果は次のようになります。

2 : 3 1 : 3 0 : 3

実装は次のようになります。行数的には with-defer2 より若干長いですが、関数の判定といった辛い作業がないため、かなり気楽な実装になっています。なお、読んだことのある方は分かると思いますが、Let Over Lambda の defmacro! マクロの実装やインタフェースを多いに参考にしています*3。

( defun flatten ( x ) ( labels (( rec ( x acc ) ( cond (( null x ) acc ) (( atom x ) ( cons x acc )) ( t ( rec ( car x ) ( rec ( cdr x ) acc )))))) ( rec x nil ))) ( defun c!-symbol-p ( s ) ( and ( symbolp s ) ( > ( length ( symbol-name s )) 2 ) ( string= ( symbol-name s ) "C!" :start1 0 :end1 2 ))) ( defmacro with-defer! ( &body body ) ( let (( g-defer-list ( gensym "DEFER-LIST" )) ( g-body ( gensym "BODY" ))) `( let ( ,g-defer-list ) ( macrolet (( defer ( &body ,g-body ) ( let ( ( syms ( remove-duplicates ( remove-if-not #'c!-symbol-p ( flatten ,g-body ))))) `( push ( let , ( mapcar ( lambda ( s ) `( ,s , ( intern ( subseq ( symbol-name s ) 2 )) ) ) syms ) ( lambda () ,@,g-body )) , ' ,g-defer-list ) ))) ( unwind-protect ( progn ,@body ) ( dolist ( func ,g-defer-list ) ( funcall func )))) ) ))

この方法であれば、 defer の中に式が複数あるとダメとか let のようなスペシャルフォームで包むとダメといった制約はありません。

( with-defer! ( dotimes ( i 3 ) ( defer ( print c!i ) ( let (( x 100 )) ( print ( + c!i x )))))) 2 102 1 101 0 100

こうして様々な「構文」をいじって遊べるのは Lisp の面白いところですね。