Picrin:マクロシステム

Picrin:マクロシステム

Picrinのマクロシステム

Picrinのマクロシステムを今までの不完全なものからついに置き換えたのでどういう仕様かを説明する。

例1. swap!

簡単なswap!マクロをPicrinのマクロで定義してみる。

(define-syntax (swap! a b)
  #`(let ((value #,a))
      (set! #,a #,b)
      (set! #,b value)))
> (define a 1)
> (define b 2)
> (swap! a b)
> a
2

デフォルトで健全なので、一時変数valueはaやbと衝突を起こさない。let、set!、およびvalueは#,でアンクオートされていないので、自動でリネームされる。

> (define value 1)
> (define a 2)
> (swap! a value)
> a
1
> b
2

ちなみに、swap!の定義は以下のようにも書ける。この辺はdefineに似ている。

(define-syntax swap!
  (lambda (a b)
    #`(let ((value #,a))
        (set! #,a #,b)
        (set! #,b value))))

例2. aif

このマクロはデフォルト健全だが不健全なマクロも簡単に記述できる。

(define-syntax (aif test then else)
  #`(let ((#,'it #,test))
      (if #,'it
          #,then
          #,else)))

「生の」シンボルitを束縛に使っている。こうするとitはthen節やelse節と同じ文脈にある変数だと認識される。

ついでに、このaifを拡張して、else節を省略できるようにしてみる。else節を省略した場合は#fになるとする。

(define-syntax (aif test then . else)
  #`(let ((#,'it #,test))
      (if #,'it
          #,then
          #,(if (null? else)
                #f
                (car else)))))

このように、#,の後ろにはどんな式でも書ける。もしelse節を省略した時の値に興味が無い場合は以下のようにも書ける。

(define-syntax (aif test then . else)
  #`(let ((#,'it #,test))
      (if #,'it
          #,then
          #,@else)))

ところで、これまで出てきた#' #` #, #,@という4つの構文は、リーダーがそれぞれリストへと変換する。

この辺りの挙動は普通の' ` , ,@と同じだ。

例3. cond

condマクロを定義してみる。とりあえず、elseも=>もサポートしないものを作ってみる。

(define-syntax (cond . cs)
  (if (null? clauses)
      #f
      #`(if #,(caar cs)
            (begin #,@(cdar cs))
            (cond #,@(cdr cs)))))

これに、elseのサポートを追加してみる。

(define-syntax (cond . cs)
  (if (null? clauses)
      #f
      (if (and (variable? (caar cs))
               (equal? (caar cs) #'else))
          #`(begin #,@(cdar cs))
          #`(if #,(caar cs)
                (begin #,@(cdar cs))
                (cond #,@(cdr cs))))))

variable?とvariable=?の2つはここで初めて出てきた。define-syntaxで式を受け取ったとき、その式はもとの式に環境情報がくっついた式になっている (具体的には、入力の式の中の変数要素がすべてvariableという型に持ち上げられている)。入力の式にくっついた環境情報は各マクロ呼び出しが終わるたびに剥がされるので展開結果に環境情報が混ざることついて変な心配をする必要はない。だがマクロ定義のなかで実際に変換する処理を書く時には環境情報の存在を意識しておく必要がある。実際、ここでは古典的マクロであればsymbol?とeq?と書くところを、variable?とequal?と書いた。

(注意:実際には、elseはマクロか値によって束縛されていないといけない。Picrinの場合はこんな感じ。

(define-macro else
  (lambda _
    (error "invalid use of auxiliary syntax" 'else))))

define-macroは最もプリミティブなオペレータで、これから説明する。なんでelseが束縛済みでないといけないかも、これから説明する。)

例4. define-record-constructor

Picrinのマクロの使い方は基本的には古典的マクロと同じだがいくつか注意すべき点がある。Picrinの内部で使われているdefine-record-constructorというマクロがいい例になるのでここで紹介してみる。

Picrinのdefine-record-typeはコンストラクタ、述語、アクセサを定義する式に展開される。その中でコンストラクタを生成するマクロは以下のように定義されている。

(define-syntax (define-record-constructor name . fields)
  (let ((record #'record))
    #`(define (#,name . #,fields)
        (let ((#,record (make-record)))
          #,@(map (lambda (field) #`(record-set! #,record '#,field #,field)) fields)
          #,record))))

例えば

(define-record-type pair
    (cons car cdr)
    pair?
  (car car set-car!)
  (cdr cdr set-cdr!))

というレコード型定義は以下のコードを含む式に展開されて、

(define-record-constructor cons car cdr)

これがさらに展開されると以下のような感じになる。

(define (cons car cdr)
  (let ((record (make-record)))
    (record-set! record 'car car)
    (record-set! record 'cdr cdr)
    record))

ただし、展開後の式中のdefine, let, record, make-record, record-set!は適切にリネームされている (なのでこの式は実際の展開結果から構文情報を剥がしたもの)。

さて、define-record-constructorの定義は少し分かりづらいがよく見てみると少しトリッキーなことをしている。一時変数recordをswap!の時のようにsyntax-quasiquote(#`)の中に直接記述せずにわざわざ一度生成した構文情報付きのシンボルをsyntax-unquote(#,)で埋め込んでいる。

; こういう定義をしているけれど
(define-syntax (define-record-constructor name . fields)
  (let ((record #'record))
    #`(define (#,name . #,fields)
        (let ((#,record (make-record)))
          #,@(map (lambda (field) #`(record-set! #,record '#,field #,field)) fields)
          #,record))))

; これで良さそうに見える
(define-syntax (define-record-constructor name . fields)
  #`(define (#,name . #,fields)
      (let ((record (make-record)))
        #,@(map (lambda (field) #`(record-set! record '#,field #,field)) fields)
        record))))

実は、一時変数をsyntax-quasiquote内で宣言した場合、その有効範囲はそのsyntax-quasiquoteの中だけに限られる。今回の場合、record-set!の第一引数でrecord変数を使う式は、最初にrecord変数を宣言した式とは別のsyntax-quasiquoteの中に存在するので、2つのrecord変数は別ものとして解釈される。このようなマクロを定義したい時は最初の定義のようにrecord変数を一度生成してからそれを使いまわす必要がある。こうすることで、別々のsyntax-quasiquote内にあるrecordが同じものを指すということをマクロ展開器に教えることができる。


Picrinのマクロを定義するためにこれから新たに覚えるべき手続きと構文はここまでで説明した6つで全部だ。

この6つ(とcarやcdrやequal?のような入力式を操作するための関数)があればどんなマクロも書ける。ただし複雑なマクロを書こうと思うと内部の実装を理解しておかないといけない。

identifer型・variable型

Picrinには普通のschemeにあるpairやnull、vector、symbolといった型に加えてidentifier型とenvironment型を提供している。

environment型は一つ一つの構文環境を表す。lambdaでスコープを作るとそれに対応したenvironmentが作られる。environmentはschemeの世界から普通のオブジェクトと同じように扱える第一級の値だが、それを操作するための手続きは処理系から一切提供されていない。コンストラクタもアクセサーもないのでenvironmentを新しく作ったり、中身を覗いて書き換えるなんてことはできない。

identifier型はvariableとenvironmentをフィールドとして持つ型だ。variableとはなにかと言うと、identifierかsymbolのことだ。つまりschemeのlistが「nullかlistをcrdに持つpair」であるように、variableは「symbolかvariableをフィールドとして持つidentifier」のことだ。identifier型の定義はdefine-record-typeで書けばこうなる。

(define-record-type identifier
    (make-identifier var env)
    identifier?
  (var identifier-variable)
  (env identfiier-environment))

variableの正体が分かったのでvariable?の定義はこんな感じだとわかる。

(define (variable? obj)
  (or (symbol? obj) (identifier? obj)))

equal?でvariable同士を比較した場合内部でvariable=?という手続きが呼ばれる。variable=?は2つのvariableが同じ場所を指すか変数どうかを判定する。細かい説明はマクロ展開器の話をするまではお預けにする。

(define (variable=? var1 var2)
  (eq? (resolve var1) (resolve var2)))        ; ※あくまでイメージ

ここに挙げた6つでPicrinが提供するidentifierとvariableの手続きは全てだ。;;; environmentに関連する手続きは0個なのでenvironmentの手続きを含めても6つだ;-)

Picrinの低レベルマクロシステム

実は最初に挙げたPicrinのマクロのための5つの構文(define-syntax, syntax-quote, syntax-quasiquote, syntax-unquote, syntax-unquote-splicing)はマクロとして定義されている。今まで使ってきたマクロシステムは本当はもっと低レベルなマクロシステムの上に構築された高レベルマクロだったということだ。swap!マクロを低レベルマクロで定義すると以下のようになる。

(define-macro swap!
  (lambda (form env)
    (let ((a (car (cdr form)))
          (b (car (cdr (cdr form))))
          (tmp (make-identifier 'tmp env)))
      `(,(make-identifier 'let env) ((,tmp ,a))
        (,(make-identifier 'set! env) ,a ,b)
        (,(make-identifier 'set! env) ,b ,tmp)))))

define-macroはマクロを定義するための最も低レベルなオペレータだ。第2引数の手続きをマクロとして登録する。例えば(swap! x y)のようにしてマクロを使う式が現れると、マクロ展開器はその式とマクロが使用された構文環境によって、マクロに登録された手続きを適用する。手続きが返した値がそのままマクロ展開結果となり、元の式があった場所にあたかも最初からそうだったかのように埋め込まれる。

TODO

More ...