Gauche でリードマクロ?

プログラミング言語 Scheme にはリーダの挙動を変更する方法が用意されている。 R7RS には #!fold-case#!no-fold-case が定義されており、リーダはこの識別子に出逢うと大文字小文字を区別しないモードと区別するモードとが切替わるようになっている。 これらは慣例的にハッシュバンディレクティブなどと呼ばれる。 処理系によってはこのふたつ以外のディレクティブを持っていることもある。

もちろん Gauche にもこの機能があるのだが、規定のディレクティブを持っているだけでなく新しいディレクティブを定義することも可能であることを発見した。 公にドキュメントに記述されていないのでユーザが使うことを想定していないのだとは思うが define-reader-directive によってディレクティブとリード手続きを結び付けられる。

たとえば以下のように定義すれば XML をプログラム中にあたかもリテラル表現であるかのように記述できる。

(use sxml.ssax)

(define-reader-directive 'xml
  (^(sym port ctx)
    (list 'quote (ssax:xml->sxml port '()))))

(print #!xml <a><b>hoge</b>huga</a>)

リーダが #!xml という識別子に遭遇すると後続するひとつの XML を読込み SXML 形式のリテラルに置換える。

実行するとこのように表示されるだろう。

(*TOP* (a (b hoge) huga))

Gaucheソースコード中ではリーダの挙動を管理するフラグを切替えるとすぐに戻るような使い方しかされていないのだが、戻り値があればそれをソースコード中に埋込むことが出来るようだ。

注意すべき点としては、先読みでポートから文字を消費してしまうようなパーサを埋込むとまともに使えないので既存のパーサをそのまま利用できないことがあることだろう。

もうひとつの使用例としてヒアドキュメントも紹介しておこう。

(use srfi-13)

(define-reader-directive 'heredoc
  (^(sym port ctx)
    (let1 delimiter (string-trim-both (read-line port))
      (do ((line (read-line port) (read-line port))
           (document '() (cons line document)))
          ((string=? line delimiter)
           (string-concatenate-reverse (intersperse "\n" document)))))))

(display #!heredoc END
hello
world
END
)

ディレクティブ #!heredoc の直後にある識別子を終了タグとし、次の行から終了タグに出逢うまでをひとつの文字列として読込む。 文字列として記述するのと違ってどの文字を書くにもエスケープを必要としない。 一方でエスケープシーケンスは使えない。

リーダの切替えが出来るのはとても面白い機能だと思う。 繰返しになるが define-reader-directive はドキュメントに記載されていない非公式な機能であることには充分に配慮する必要がある。 本格的なプロジェクトには使わない方がよいだろう。

Document ID: 7e4794fab151003c490949bfc75aaa24