do マクロの使い方を覚えられないクラスタです。
なかなかこれが、Schemeでは再帰ばっか練習するんで、
do
は意識して練習しないとdo
嫌いに成りかねません。まあ、以前も言ったんですけど、スタイル的には実は
do
はnamed-let
の変種です。Common Lispの内部では再帰とは全く違う破壊的な計算を行ないますが、かと言って、スタイルだけに注目すれば、実はnamed-let
とはそんなに違いがないのです。僕の中では
- 横に広がる
named-let
- 縦に伸びる
do
とか言ってました。意味分かんないっすね(笑)。まあ、変数束縛の位置が、って事なんですけれども。あと、
do
がフグみたいに見える、ってんでフグ構文とか呼んでたりしました(笑)。do
の一般形は次のようになっています。ね、フグみたいでしょ(笑)?
Schemeの
named-let
の一般形は次の通りです。つまり、基本的には変数、初期値、ステップの配置が違うだけ、です。
do
だとまとめて記述する。named-let
だとそうじゃない、って事ですね。どっちがどっちより便利、って事は基本無いわけなんですけれども。
ただ、個人的には、ここで言う副作用とは、例えば入出力であるとか、そう言うケースですが、副作用が関わる式、特に出力を実行しなければならない場合、
do
の方がよりシンプルに書ける場合が多いとは思います。named-let
だと、スッキリ決まらないケースが多くて、そう言う場合、do
の方がシックリくる場合が多いような気がします。上の例はあまり良くないんですが、と言うのも、
named-let
はcond
を使って暗黙のbegin
で上手い具合にみっともなさを回避してるんですが、基本、named-let
のシンプルさはぶち壊れてるんです(笑)。目立ちませんが(笑)。反面、後者の
do
ヴァージョンは本体部に「計算以外の余計な作業」をまとめられます。これはシンプル過ぎる例なんですが、長く余計な作業がある、って事はままあるんです。そう言う時、さすがの再帰構文でもシンプルに書けなくなる。いや、構造はシンプルなんですが、コード自体は汚く見える場合があるんですよね。いや、すまない。いい例が思いつかなかった(爆)。ただ、普段は再帰で構わないけど、いざとなったら
do
の方がシンプルに書ける場合があるんだよ、って事です。はい。
注:いや、ホントに下手な例でゴメン。というのも、do
に関して言っても、カウンター内で出来る事は全てやってしまうのがスタイル的には美しいんですが、それで悩むんだったら素直に再帰した方が良い、ってのが事実。要するに再帰での「引数内のカウンター処理だけじゃどうしようもない」部分が出てきた場合、do
の出番だ、って言い方の方が正しいかも。
さて、valvallowさんの記事によると、どうやら「
NIL
じゃない返り値が欲しい」との事。まあ、これは当然でしょうね。ポール・グレアムが何でマクロで書いた
while
でNIL
以外の返り値を返すようにしなかったのか?想像するに、二つ程理由が考えられて、マクロの導入章(第7章なのに!)の辺りなんで、あんまややこしい例じゃなくってシンプルな例にしたかった事。あとは破壊的変更が前提のマクロなんで、返り値を返すとマズイ、って事があったんでしょうね。グレアムの
while
は次のようにしてみれば面白い結果が出てきます。while
の指定はi < 10
の筈なのに、何とi
は10になっています。つまり、返り値になるのはこのi
になるのは自明でしょう。これはマズいです(笑)。恐らく返り値は9であって欲しい、ってのが皆願う事でしょうから。だから破壊的変更が前提なら、返り値を
NIL
でもしとけば良い。堅実な判断ですよね。さて、それでは返り値を、Common Lispらしく最後に実行された値を返すように改造していきましょう。まずは、LOLでお馴染みでしょうが、関数
last
とbutlast
ってのを使ってみます。関数
last
はリストの最後の値のリストを返し、関数butlast
はリストの最後の要素を除いたリストを返します。今何でこれを使おうと思ったのか、と言う理由は、繰り返しが何回実行されようと、必要なのは計算の最後の作業結果だという事だからです。例えば、上のvalvallowさんの例だと、欲しいのは式をして実行された
(print i)
と(incf i)
全体じゃなくって、あくまで(incf i)
だけ、なんです。言い換えるとbody
の最後の計算結果さえ分かっていれば良い、と言う事です。すなわち、
body
を二つに分けちゃう。最後と、それ以外、です。このアイディアで雛形は一応次の通りになります。
では実行してみますか。
やっぱりダメだ(笑)!
body
を二つに分ける、ってアイディアは秀悦だとは思うんですが、返り値が10になってます。こりゃイカン。この理由は、終了条件が調べられる前に
var
が更新されちゃうから、なんです。つまり、テスト式に行く前にi
が10になってしまう。ここを直さないとどーにもならん、わけです。さて、ここでやっつけ仕事のハック。嫌でも
var
が更新されちゃうんだったら、その前の状態を保存しとけばエエんちゃうの?ってのがアイディア。つまり、変数を二つ用意しちゃうんだ!!!一つは今まで通り(last body)
の更新用。もうひとつは、前回のそれの保存用だ!これでどうだ。では実行してみましょう。
上手く行きましたね。大成功!!!これで終わり……とはいかないんですよ、残念ながら(笑)。
実はこのマクロは次のような問題があるんです。
これが変数衝突ですね。つまり、
my-while%%
マクロの内部でvar0
、var1
って変数名を使ってるわけなんですけど、これが外部から与えられると途端にぶつかってしまってどうにもこうにも行かなくなる。つまり、これを避けるのが
gensym
です。だから、こう書かないとなりません。これで安心してどんな変数名を
my-while
マクロ内に持たせても大丈夫です。gensym
をいつ使うか?基本的にはいつでもです。意図してアナフォリックマクロを書く時以外はバンバン使って構わないと思います。あって困るもんじゃない。基本的には、
defmacro
で引数として与えられた変数以外で、テンプレート内に「いきなり」現れる名前全部に対してgensym
を使って構わない
と言うのが原則です。複雑なマクロでテンプレート内にバンバン新しい変数が現れる場合は
gensym
だらけになりますが、それで良い、のです。繰り返しますが、gensym
はあって困るもんじゃない。むしろ無けりゃ困るんです。まあ、もっともみっともなくなる可能性もありますが、その回避の為にOn Lispでは
with-gensyms
というコードが紹介されています。また、上のmy-while
はLOL流にdefmacro!
を用いれば次のように記述されますね。ちなみに、再帰版は次のようになるでしょう。
0 件のコメント:
コメントを投稿