見出し画像

Retro-gaming and so on

伝統的マクロの良くあるパターンとSchemeの衛生的マクロへの翻訳

Lispの基本を考えよう。

Lispで書かれたプログラムは、大枠で言うと次の二種類の「データ型」によって記述されている。

  1. リスト
  2. アトム
あんま昨今「アトム」と言う言い回しは見ないが、単純に言うと「リスト以外のデータ型」をアトムと呼ぶ。
「Lispのプログラムはたった二種類のデータ型で構成されてるなんて凄い!」と言う感想を引き出したいわけじゃない(笑)。大体、「リスト以外のデータ型」は多岐に渡るわけで、アトムのサブカテゴリには当然山のようなデータ型が存在するわけだ。シンボル、数値、文字列、ベクタ(配列)、等は全てアトムに属する。
結果、「Lispのプログラムはたった二種類のデータ型によって構成されています」なんて言い回しはある意味インチキだ(笑)。
しかし、Lispが他のプログラミング言語と違う特徴があるのは次から出てくる。通常、プログラムはテキストファイル上で書かれている。そしてインタプリタないしはコンパイラにそのテキストファイルが読み込まれて、「字句解析」「構文解析」によって、使用しているプログラミング言語の「要素」に分解されるわけだ。カッコ付けて言うと「抽象構文木」が生成され、そこでやっと「プログラム解釈前」と言う下準備に到達する。
一方、Lispの場合、Lispプログラムが書かれたテキストファイルが読み込まれた時点で既にLispプログラムとして「意味がある」構造を得る。と言うのもテキストファイルが読み込まれた時点でそこに「書かれている」ブツは全部「データ型」として確定する。要は上に書いた通り、ソースコードは「リスト」と「アトム」としてすぐさま解釈されるわけだな。Lispでは抽象構文木を生成する、なんつー手間が要らない。もっと言っちゃうと、Lispプログラム自体が抽象構文木そのものを表している。S式だ。よって、読み込まれた時点で「意味があるプログラム」になってるんだ。
いずれにせよ、フツーのプログラミング言語で書かれたソースコードとその言語処理系の「処理」との距離は、Lispプログラムを書いたソースコードとLisp処理系の「処理」との距離より遠い。その差は東京〜八王子間と東京〜大阪間くらい違う。

唐突にここでマクロの話をしよう。
プログラミング言語の中には「マクロ」と呼ばれる機能を持ってるモノがある。
代表的なトコにC言語がある。
例えば、C言語のマクロは次のように記述する。

#define DATA_NUM 10

#defineと言うのはプリプロセッサ指令と呼ばれるブツの一種で、マクロを定義する為に使われる。
で、マクロと言うのは、ソースコード上に、例えば上の例だとDATA_NUMと記述された部分を見つけると粛々と10に置き換えていく機能だ。
例えば、C言語で書かれた次のような記述があるとする。

int data[DATA_NUM] =
{
   92, 94, 90, 80, 81, 82, 84, 79, 75, 95
};

そうすると、C言語処理系はコンパイル前に、マクロの指定に従って、上のソースを次のように書き換える。

int data[10] =
{
   92, 94, 90, 80, 81, 82, 84, 79, 75, 95
};

いわゆるプログラミング言語上の「マクロ」のやる事は簡単だ。単に指定に従ってソースコードの特定の文字(あるいはトークンと言って良い)を置き換えるだけ、だ。
じゃあLispで言う「マクロ」はどうなんだろう。Lisperは「Lispのマクロは特別/特殊だ」と言いたがる。しかし実際、表層的な意味/機能に於いてはLispのマクロもC言語のマクロも変わらない。「ソースコード上の特定のトークンを置き換える」って意味では両者ともやってるこたぁおんなじなんだ。

CL-USER> (defmacro data-num () 10)
DATA-NUM
CL-USER> (make-array (data-num))
#(0 0 0 0 0 0 0 0 0 0)

一方、LispのマクロをLisperが「特殊なモノ」と言いたくなる真の原因は、むしろ、上で書いたような「Lispプログラムはソースとして読まれた時点で全てがデータ型で構成されている」と解釈される事に負うトコが多いだろう。つまり、Lispプログラムのソースコードの構成は抽象的で無力な「トークン」の組み合わせを意味していない。結果、C言語みたいに「文字の組み合わせ」を置き換えてるんじゃなくって「プログラムをプログラムで」置き換えるような事になってるわけだ。
言い換えると、「Lispのマクロは凄い」んじゃなくって「Lisp自体が凄い」んだ。Lispは「抽象構文木」を直接プログラムするようなプログラミング言語なんで、表層的にはC言語のマクロと変わらん事をやってても、それが作り出す「結果」は他の言語じゃできない事をやってのける事となる。

さて。
繰り返すが、Lispのマクロはある特定のシンボル(マクロ名)を見かけた時にそれをプログラムに置き換える事をやってのけている。そしてマクロは具体的には(多くのケースでは)リストとシンボルの組み合わせを生成するように指定したり、あるいはシンボルを「並べ替えたり」する。
そして、Lispのマクロの「材料」はLispプログラムの「構成要素」となっている。ANSI Common Lispの「伝統的マクロ」と言うのは「それだけ」のシステムだ。全部がANSI Common Lispに「内包」されていて、例外の部品は存在しない。
いや、これはANSI Common Lispのマクロの大きな特徴で、マクロだろうと何だろうと、例えばシンボル(以外もそうだけど)操作はLispプログラミングの枠をはみ出していない。「マクロは書くのが難しい」のは事実なんだけど、「部品の操作」って意味ではフツーに関数を書くのと何ら違いはないんだ。
繰り返すけど、それが「伝統的マクロ」と言うシステムだ。

一方、Schemeだと事情が変わってくる。Schemeで取り上げられている「衛生的マクロ」の部品は、誤解を畏れずに言っちゃうと、Schemeと言うプログラミング言語に内包されていない。SchemeはSchemeと言うプログラミング言語と「衛生的マクロ」と言うプログラミング言語の混成体だ。事実上、2つのプログラミング言語が合体している。
そして、ANSI Common Lispのマクロは、リストと(主に)シンボルを操作してるが、Schemeの「衛生的マクロ」はリストやシンボルを操作してるわけじゃない。見た目は似てるが、実際はリストの代わりにsyntax、シンボルの代わりにsyntax-objectと言うブツを操作している。
結果、Schemeの「衛生的マクロ」はLispに見た目は似てるけど、それはLispじゃないんだ。繰り返すが「衛生的マクロ」はSchemeとは全く別のシステムを持った「Lispに見かけを似せた」プログラミング言語、ってのが実態だ。
そしてそのためSchemeの衛生的マクロでは、ANSI Common Lispのマクロみたいに「Lispの部品をフツーの関数プログラミングのように」操作出来ない。何故ならsyntaxはリストではないし、syntaxがリストじゃない以上、syntaxをリストのように操作出来ないから、だ。

ところで「衛生的」とは一体何が衛生的なんだろう。
その形容詞の意味の多くは伝統的マクロのシンボルが持つ「危険性を回避した」トコにある。伝統的マクロの場合、原則、マクロ内で使われているシンボルを保護していない。何故ならそれは単なる「Lispプログラムの部品」であって、何らフツーのシンボルと差がないから、だ。
だから下手すればマクロ内で使われたシンボルとそのマクロを使ったプログラムが使用しているシンボルが偶然同じモノで、「名前の衝突」を起こす可能性がある。
他にもマクロで再帰しようとしたら無限ループに陥ったり、あるいは特定のシンボルを一回だけ評価しようとしたら複数回評価されちゃう、とか「Lispプログラムとしてフツーに書けるマクロ」のお陰でとんでもない動作が起きる危険性があるわけだ。
Schemeの「衛生的マクロ」はこれを全部避けよう、と言う試みで、結果として「Lispに含まれない材料」を用いてこれらアレコレを回避しようとしてるわけだ。Schemeの「衛生的マクロ」で一見シンボルに見えるモノがシンボルじゃないなら、そもそもシンボルにまつわる問題が起きない。そしてリストが存在せずsyntaxと言うデータ型が基本なら、「syntaxの再帰」なんてあり得ない以上、再帰が終了せずに無限ループが起きる、と言う問題も生じない(つまり、syntaxを再帰状態にするような構文がそもそも存在しない)。
それらが「衛生的マクロ」の特典なんだけど、同時に、「Lispに存在しないデータ型」を多用する為に、色々とクソ面倒臭い問題が別に生じるわけだ。

Schemeのsyntax-rulesに慣れてくると、例えばポール・グレアムのOn Lispのコードを移植してみようか、とか思うのは皆通る道だと思う。

 

ところが、それがなかなか上手く行かない、ってのも皆経験すると思う。
特に初見で困るのが、ANSI Common Lispのマクロではmapcar(Schemeでのmap)を多用する事だ。「え、一体何これ?」ってのが皆経験する事だろう。
繰り返すがANSI Common LispのマクロはフツーのLispプログラミングのテクニックをそのまま使える。そして、ANSI Common Lispのマクロでは「リストを望み通りの形式へと処理/変形する」為にmapcarは頻出なんだ。
例えば、On Lispでマクロ内にmapcarが登場する最初の例として、letの再実装が紹介されている

(defmacro our-let (binds &body body)
 `((lambda ,(mapcar #'(lambda (x)
           (car x))
      binds)
  ,@body)
 ,@(mapcar #'(lambda (x)
        (cadr x))
      binds)))

こういうmapcarを多用したパターンがANSI Common Lispのマクロでは多い。
なお、ここではOn Lispで記述されたour-letより構成を簡単にしている。ラムダ式内のconspに依る条件判断にまつわるアレコレを削除している。一つは、ANSI Common Lispのマクロの頻出パターンを明解にする為、と、もう一つはSchemeのletに合わせる為だ。
実はANSI Common LispのletはSchemeのそれと違い、使用時に初期値を与えない事も可能なんだ。

;; Schemeではx、yには必ず初期値を与えないとならない
> (let (x y) (cons x y))
. let: bad syntax (not an identifier and expression for a binding) in: x
;; 一方、ANSI Common Lispでは初期値ナシでもO.K.だ
CL-USER> (let (x y) (cons x y))
(NIL)

この処理をする為に、(x y)(binds)の各要素(xy)がconsなのかconspで調べてるわけだ。
話を戻そう。
ポール・グレアムが書いたour-letのコードを真に受けて、Schemeで次のようにコードを書くと失敗する。

> (define-syntax our-let
  (syntax-rules ()
   ((_ binds body ...)
   ((lambda (map (lambda (x)
          (car x))
          binds)
    body ...)
   (map (lambda (x)
      (cadr x))
      binds)))))
> (our-let ((x 1) (y 2))
     (+ x y))
. lambda: not an identifier, identifier with default, or keyword in: 
(lambda (x) (car x))

失敗の理由は明らかで、まずはsyntax-rulesを使用した時点で以下のブツはリストではなく全部syntaxだ、と言う事だ。
そしてポール・グレアムが示したコード内のmapcarが形作る式は「評価タイミング」が違う。バッククオート(`)はリストを生成するが、アンクオート(,)が生成するコード(つまりmapcarが処理する式)は最初に処理される。しかしながらSchemeで書いたコードは全部syntaxな為、そのようなタイミングのズレがない。
もうちょっと何とかしよう、と、例えばもっとポール・グレアムのコードを真似て次のように書いたとしよう。

> (define-syntax our-let
  (syntax-rules ()
   ((_ binds body ...)
    `((lambda ,(map (lambda (x)
            (car x))
          binds)
     body ...)
    ,@(map (lambda (x)
         (cadr x))
       binds)))))
> (our-let ((x 1) (y 2))
(+ x y))
. . x: undefined;
cannot reference an identifier before its definition

実はこう書いちゃうと「リストを生成」はするんだけど、「マクロとして実行」はされない。
よって「xが定義されてません」と言うようなおかしなエラーに遭遇するわけだ。
元々、ポール・グレアムのコードではラムダ式に与える「仮引数」を、bindと言うリストから「取り出す」為にmapcarを使用してて、要はリストから要素を「取り出して整形」してたわけだが、それはSchemeの衛生的マクロでは上手く行かないわけだ。
いずれにせよ、この、ANSI Common Lispのマクロで良く遭遇する「mapcarを使ったパターン」はSchemeのsyntax-rulesだと直訳出来ない。
しかしながら、上のour-letsyntax-rulesのパターンマッチング機能により次のように書ける。

> (define-syntax our-let
  (syntax-rules ()
   ((_ ((car-x cdr-x) ...) body ...)
    ((lambda (car-x ...)
     body ...)
    cdr-x ...))))
> (our-let ((x 1) (y 2))
     (+ x y))
3

ポール・グレアムのコードだとbindsと言うシンボルで抽象的にour-letに束縛される変数群を想定した。
一方、syntax-rulesでは「bindsが想定している形式」を具体的にそのまま記述している。そしてそれらを「分解して」所定の位置にハメ込む事でマクロour-letを実現するわけだ。
結果、ANSI Common Lispで記述されたマクロより遥かにシンプルなマクロがsyntax-rulesでは記述出来る。意味もANSI Common Lispのそれより明解だろう。
このように、syntax-rulesのパターンマッチングは「ハマれば」絶大な威力を発する。「ハマれば」と言う事は「ハマらない」時には非力も非力になる、って事だ。
そもそもsyntax-rulesのパターンマッチングは、syntax-rulesの設計者が想定した範囲内であればパワフルだ、と言う割に当たり前の弱点がある。そう、「想定されてない」パターンには対応出来ないわけだ。
ここではそういった「想定外パターン」でmapcarが使われてるようなANSI Common Lispの「伝統的マクロ」をどうやってSchemeの健全なマクロへ移すか考慮してみよう。そしてそういった「想定外パターン」は伝統的マクロでは「割に良くある」パターンなんだよ。

ここではサンプルとして独習Scheme三週間で扱われていたambマクロを見てみる。
amb自体は以前このブログでも取り扱った事がある。なお、その記事を書いた時点では語源がわからなかったが、ambはAmbiguous(あいまいな)の略称らしい。
さて、独習Scheme三週間で紹介されているambマクロは次のようになっている。

(define-macro amb
 (lambda alts...
  `(let ((+prev-amb-fail amb-fail))
   (call/cc
    (lambda (+sk)

     ,@(map (lambda (alt)
          `(call/cc
           (lambda (+fk)
            (set! amb-fail
             (lambda ()
              (set! amb-fail +prev-amb-fail)
              (+fk 'fail)))
            (+sk ,alt))))
        alts...)

     (+prev-amb-fail))))))

さてさて。Schemeで書かれたambマクロ、と言いながらその記述の趣は「衛生的マクロ」とは随分と違う。と言うか、ANSI Common Lispの伝統的マクロそっくりだ。
理由はこうだ。実はこの文書、原文は1998年に書かれたモノだ。そして同年、R5RSが発表され、初めて公式にSchemeに「衛生的マクロ」が導入された。
つまり、独習Scheme三週間、と言う文書は、前提としてR4RS/IEEE Scheme制定時に書かれたモノなんだ。そしてこの時点ではSchemeには正式にマクロは存在しない、あるいは「仕様上のオマケ」だったんだ。
そしてこの時代、各種Scheme実装ではそれぞれが独自にANSI Common Lispを参考にしたマクロを持っていた。要は実装同士だと「互換性がない」オリジナルマクロに溢れてたわけだな。
まぁ、そんなワケで、今では見かけないANSI Common Lispスタイルのマクロになっている。
なお、独習Scheme三週間が想定しているScheme実装はRacketの前身であるMzSchemeなんだけど、現在このマクロのスタイルはRacketでは非推奨になっている。
さて、この「古臭いマクロ」を衛生的マクロでどう書き直すのか、と言うのがお題なんだけど、ここでもANSI Common Lisp的にmapが使われていて翻訳がかなり厄介だ。
mapはリストを生成する関数だが、ここではリストを生成するのが目的ではない、と言う辺りがその厄介さを生んでいる。
アンクオート・スプライシング(,@)はバッククオート(`)が生成するリスト内で、その内側を実行した後、カッコを外す機能だ。

;; アンクオートは計算結果のカッコを保持する
> `(1 ,(map identity '(2 3 4)))
'(1 (2 3 4))
;; アンクオート・スプライシングは計算結果からカッコを外す
> `(1 ,@(map identity '(2 3 4)))
'(1 2 3 4)

つまり、上のamb内のmapはリストが欲しくて使われてるわけじゃない。事実上、逐次処理を行いたいが為、埋め込まれてるんだ。
要は、ambマクロに与えられる引数がalt0alt1、・・・と言った時、次のような「関数実行」が連続で行われるようにしたい、と言うのがその意図となっている。

((lambda (alt)
 `(call/cc
  (lambda (+fk)
   (set! amb-fail
     (lambda ()
      (set! amb-fail +prev-amb-fail)
      (+fk 'fail)))
   (+sk ,alt)))) alt0)

これは引数がalt0だが、続いてalt1が引数、また続いてalt2が引数・・・と「全く同じ関数」が引数を全部消費するまで展開される。そしてそれらが全部「逐次実行」され、最後に+prev-amb-failが実行される、と。
これがsyntax-rulesでは書けない。バカ正直に

(define-syntax amb
 (syntax-rules ()
  ((_ alts ...)
  (let ((+prev-amb-fail amb-fail))
   (call/cc
    (lambda (+sk)
     (map (lambda (alt)
        (call/cc
         (lambda (+fk)
          (set! amb-fail
            (lambda ()
             (set! amb-fail +prev-amb-fail)
             (+fk 'fail)))
          (+sk alt))))
     `(,alts ...))
    (+prev-amb-fail)))))))

と書いても上手く動かない。先にも書いたが、,@が付いたmapはマクロ実行時より先に式が実行されてなければいけないんだけど、これだとmap実行時はマクロ実行時と同時だ。また、繰り返すが、伝統的マクロ版amb内でのmapはリストが欲しいわけじゃなく、単に逐次実行したい関数での計算を並べまくる為に存在する。
じゃあ、syntax-rulesの引数のパターンマッチに頼る、ってのはどうだろうか。

(define-syntax amb
 (syntax-rules ()
  ((_ alts ...)
  (let ((+prev-amb-fail amb-fail))
   (call/cc
    (lambda (+sk)
     ((lambda (alt)
      (call/cc
       (lambda (+fk)
        (set! amb-fail
          (lambda ()
           (set! amb-fail +prev-amb-fail)
           (+fk 'fail)))
        (+sk alt)))) alts) ...
    (+prev-amb-fail)))))))

当然ながらこれも上手く行かない。可変長引数(alts ...)の最初の引数だけ、は関数が適用されるが、以降の引数には関数は適用されずに、単純に残りの引数が置かれるだけ、となる。
syntax-rulesのパターンマッチングは結構アホで、ホント、「想定外のパターン」に遭遇すると無力だ。このように各引数に「同じ関数を適用したい」としてもその意図を反映してはくれない。しかしながら、その「想定外のパターン」は、繰り返すが、「伝統的マクロでは良くある」テクニックなんだ。
結果、このambを「衛生的マクロ」で記述したい、となった場合、syntax-rulesを捨て、少なくとも現行のR7RSの仕様外であるsyntax-caseの力を借りる必要が出てくるわけだ。

もう一度繰り返すが、伝統的マクロはリストを組み立てる。一方、衛生的マクロはリストではなくsyntaxと言うデータ型を組み立てる。この2つは全く違うデータ型となる。
一方、syntaxを組み立てる際に、リストでのバッククオート(`)、アンクオート(,)、アンクオート・スプライシング(,@)に当たるquasisyntax(#`)、unsyntax(#,)、unsyntax-splicing(#,@)と言うモノがある。
これらを使うと、syntax-case以下のsyntax及びsyntax-objectを使ったコードの見た目を、オリジナルのambの見た目に合わせる事が出来る。

(define-syntax amb
 (lambda (stx)
  (syntax-case stx ()
   ((_ alts ...)
   #`(let ((+prev-amb-fail+ *amb-fail*))
     (call/cc
      (lambda (+sk+)
       #,@(map (lambda (alt)
            #`(call/cc
              (lambda (+fk+)
               (set! *amb-fail*
                 (delay
                  (set! *amb-fail* +prev-amb-fail+)
                  (+fk+ #f)))
               (+sk+ alt))))
           `(,alts ...))
      (force +prev-amb-fail+))))))))

なお、個人的な趣味で、無引数ラムダ式はdelayに置き換えている。無引数ラムダ式は、そこで包まれてる式を「実行させない」為にあり、結果、遅延評価の基礎であるdelayとやってるこたぁ同じ、となる。と言うか、streamを使いまくってる身としては、ここでdelayを使わないつ使うんだ?ってなカンジだ。結果、+prev-amb-fail+はプロミス塗れなんでforceで解凍してる。
もう一つ+何とか、と言う変数名が気持ち悪いんで、耳あて法よろしく+で何とかを両側から挟んでみた。
余談だけど、Lispでは、大域変数の名前は*を使って両側から挟む命名法が良く使われてるが、大域変数なだけではなく、それが「定数だ」と主張する場合、両側から+で挟んだ名前にしたりする(※1)。ここでは意味的にはそれにはあたってないが、Lispに詳しい層だと片側だけ+が存在する名前はチンコのポジションが決まらないような据わりの悪さを感じるだろう、ってぇんでこうした。
話を戻す。大枠では#`でsyntaxを形成するが、#,@の内側でmapが使われてる以上、そこが最初にS式として処理されて狙い通りの結果となる筈だ。
しかしながら、これではいまだにエラーが出る。次のようなエラーだ。

alts: pattern variable cannot be used outside of a template in: alts

このエラーに記述されているpattern variableと言うのは、ambマクロに与える可変長引数(alts ...)の事だ。こいつがまず厄介事の一つ目、だ。名前を見ると分かるが、こいつはsyntaxでもなければかと言ってLisp(Scheme)の要素でもない。あくまで「マクロ定義」に於ける「パターンマッチング機構」の要素なんだよな。
そしてそいつはtemplate、つまり構成するsyntaxの外側では使えません、と文句を言ってくる。
外枠では#`によりsyntaxを形成してるが、一方、mapが構成してるS式は#,@で「syntax外」になっている。そしてmapは第二引数にリストを取るが、`(,alts ...)ではリストとして認識されない、っつーかそれが抱えた要素が何じゃこりゃ分からんぞ、と警告しているわけだ。
このsyntaxではない、しかと言ってLisp要素ではない、ってブツを「纏めて」リストとして扱うのにはどうすればいいのか?ってのが第一の障壁となる。
こういうケースだとsyntax->listを利用し、なおかつalts ...をsyntaxとして確定させるのが手だ。

(define-syntax amb
 (lambda (stx)
  (syntax-case stx ()
   ((_ alts ...)
   #`(let ((+prev-amb-fail+ *amb-fail*))
     (call/cc
      (lambda (+sk+)
       #,@(map (lambda (alt)
          #`(call/cc
            (lambda (+fk+)
             (set! *amb-fail*
               (delay
                (set! *amb-fail* +prev-amb-fail+)
                (+fk+ #f)))
             (+sk+ alt))))
         (syntax->list #'(alts ...)))
      (force +prev-amb-fail))))))))

繰り返すがsyntax内で使われているブツはシンボルのように見えてシンボルじゃない。syntax-objectだ。従ってsyntax外でそれらを「シンボルとして使いたい」場合、頻繁なデータ変換が必要になる。上の例だとalts ...を纏めてsyntaxとして確定し、改めてmapが作用可能なリストへと変換している。
ところがこれでもまだエラーが出る。ambマクロを実行してみよう。

> (amb 1 2 3 4 5 6 7 8 9 10)
. . alt: undefined;
cannot reference an identifier before its definition

mapが使ってるラムダ式の仮引数はaltだ。キチンと定義されてる筈なのに、altが未定義だ、と文句を言ってくる。一体これはどういう事なんだろう?
これは、そこのラムダ式の本体の問題なんだ。そこで定義されているcall/cc以下は#`によって全部syntaxになっている。つまりこのままだと、ラムダ式内部で使われているaltもsyntax-objectなんだ。
一方、map以下のS式の全体はフツーのSchemeのプログラムだ。よってmapが取ってる引数のaltは単なるシンボル、って事になる。
要はシンボルのalt≠syntax-objectのaltなんだ。衛生的マクロと言うプログラミング言語からしてみれば、突然syntax-objectのaltが文脈に登場してきた、って事になる。
結果、#`が付いたcall/cc以下で使われているaltを単なるシンボルへとデータ変換しないとならない。その為にはそこのaltをunsyntax(#,)すればいい。そうすればそこだけ外部(ラムダ式)のaltと一致する。
全体のソースコードはこうなるだろう

この記事では、ANSI Common Lisp等の「伝統的マクロ」をSchemeの「衛生的マクロ」へと移植する際に、あまりにも良くある「ハマりどころ」の回避の説明をしてみた。
繰り返すが、「伝統的マクロ」ではフツーのLispプログラミングに於けるリストやシンボル操作の枠組みを逸脱しない。また「リストの要素」を整形目的で組み替えるのにmapcarを多用するケースが多い。そしてそこが、Schemeでの「衛生的マクロ」への移植での一番でのハマりどころだ。
Schemeの衛生的マクロではリストやシンボルの操作をしてない。代わりに行ってるのはsyntaxやsyntax-objectの操作だ。まずはそこを意識しよう。
そしてsyntax内とsyntax外で「同じ名前」の何かを使ってたとしても、それらは同じデータ型に属していない。適切なsyntax-objectとSchemeのデータ型間の変換を怠るとエラーを食らう。「同じ名前だから」と安心しないように。また、これが故に衛生的マクロは伝統的マクロとまた違った面倒くささを生じ、総じて伝統的マクロよりコード量が増える傾向にある。
なお、冒頭のポール・グレアム方式のletの再実装をそのままの構造でsyntax-caseで実現しようとするとこうなるmapの問題は回避されるが、一方、syntax-objectとLispのデータ構造への変換が洒落にならんくらい煩わしい(※2)。
結果、やはり通常は、syntax-rulesでパターンマッチングで解けるお題はそれでシンプルに解き、syntax-rulesどーにもならんかった時に限ってsyntax-caseに頼るのが吉だろう。

※1: Schemeには定数定義機構はないが、ANSI Common Lispには定数定義用のdefconstantと言うマクロがある。

※2: 洒落にならんくらい煩わしくなる最大の原因は例によってシンボルとsyntax-objectは同じ名前でも同一のデータじゃないから、だ。
ポール・グレアム方式だと、例えばbinds((x 1) (y 2))、body(+ x y)だと想定していても、mapでbindsを処理してる間に((x 1) (y 2))のxybodyで使われているxyと別物だと判断されてしまう。前者はmapで処理した以上シンボルだが、bodyは例によってpattern variableだから、だ。
結果、マトモに動作させる為にはどっかでデータ型としての帳尻を合わせなければならず、ここでは(body ...)と言うpattern variableをsyntaxとして確定させた後、syntax->datumでLispデータに変換した上でsyntax内から#,@で外側に出している。
要は、エラーメッセージを見つつ適宜直していく、と言う手間をかけた。
これも伝統的マクロにまつわるテクニックの代表的な例で、衛生的マクロに直訳するのが難しい典型例なんだけど、こんな苦労をするくらいなら、syntax-rulesのパターンマッチングで簡単に書いとくべきお題だと思う。
繰り返すが、syntax-caseはあくまでsyntax-rulesでどーにもならん時だけ、使うブツだと思う。
常用するには面倒くさ過ぎるんだ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事