目次へ

4. 準引用式の展開


準引用 (quasi(-)quotation) は,式のテンプレートを作るために,バッククォート「`」記号による表記を使って Scheme (R6RS 11.17) と Common Lisp (CLtL2 22.1.3) に用意されている。 L2 Lisp には,Ruby で実装された 6.0 版から導入された。

quasi-stellar object (準星) が quasar「クェーサー」と呼ばれるということもあって,quasi-quotation (または quasiquotation) を私はなんとなく「クェーサイ・クォーテーション」と読んでいますが,普通はなんと読むのでしょうか?

quasi は元々 "as if", "almost" にあたるラテン語であり, 研究社羅和辞典によれば quasī「クァスィー」,Oxford Pocket Latin 他によれば quasi「クァスィ」です。 さらに,イタリア語に基づく教会ラテン語では,s が母音にはさまれているために濁って「クァ(ー)ズィ」と発音されるはずです。
教会ラテン語ではラテン語本来の母音の長短が失われているわけですが, イタリア語式にアクセントのある母音が長めに発音されるとすればクァではなくクァーになるはずです。 ちなみに http://de.wiktionary.org/wiki/quasi によれば,quasi はラテン語で quāsī「クァースィー」,現代イタリア語で「クァーズィ」です。 ここで (古典期の) ラテン語の発音の記述に不一致があるようですが,これはラテン語本来の発音が長く忘れられていたからかもしれません。 ラテン語の長音記号は,発音を示すために辞書 (と教科書) には書かれますが,実際のラテン語の白文には書かれません。 残された手がかりと語源の推定,詩の韻律などによる制約条件から当時の発音を再構成したと思いますが, quasi については意見の一致をみていない…ということでしょうか。

英語の辞書をいくつか見ると,quasi について,ほぼどれも母音を英語式に読む発音とラテン語式に読む発音の両方を載せています。 しかし,s を濁らせるかどうか,ラテン語式に読むとき a を伸ばすかどうか,決定していないようです。 これは辞書を作るときに採られた英語話者集団でそれぞれそのような発音が通用していたと考えるのがよいかと思いますが, 年代によって,世代によって,あるいはたまたま話者の属していた文化圏の伝統によって差違があったのでしょう。 ここでの問題は,仮にそういったものがあるとすれば,Lisp の文化圏での読み方です…。

本処理系の準引用の実現方法は,Python への移植と同じく,Ruby による準引用の実現 の C# への直訳である。 Lisp 式にバッククォート記号を接頭した準引用式は,L2 Lisp では Scheme と異なり,構文つまりスペシャルフォームではない。 Common Lisp と同じく,式の読込み時に処理され,処理系本体には展開済みの式が渡される。 処理系本体は準引用について,なんら知ることはない。

本処理系では,準引用に関する処理はすべてソース・ファイル Lib_Reader.cs の中で完結している。

4.1 構文解析器クラス Reader と準引用式

準引用に関係するトークン 「,@」と「,」と「`」は,字句解析器クラス Lexer で次のように切り出される。

    internal class Lexer: IEnumerable<object>
    {
        ……
        public IEnumerator<object> GetEnumerator() {
            using (IEnumerator<char> ch = CharsFrom(lines).GetEnumerator()) {
                ch.MoveNext();
                for (;;) {
                    ……
                    char cc = ch.Current;
                    ……
                    } else if (cc == ',') {
                        ch.MoveNext();
                        if (ch.Current == '@') {
                            yield return Token.COMMA_AT;
                        } else {
                            yield return Token.COMMA;
                            continue;
                        }
                    ……
                    } else if (cc == '`') {
                        yield return Token.BQUOTE;
                    ……
                    ch.MoveNext();
                }
            }
        }
        ……
    } // Lexer

これらのトークンは,構文解析器クラス Reader の 式解析メソッド ParseExpression() で処理される。 メソッド ParseExpression() は「,」と「,@」に対し,後続の Lisp 式を引数として,それぞれ QQ.UnquoteQQ.UnquoteSplicing のインスタンスを構築する。 「`」に対し,後続の Lisp 式を引数として QQ.Expand メソッドを呼び出す。 任意の x に対し QQ.Expand(x) は準引用式 `x を展開した結果を返す。

    public class Reader: IDisposable
    {
        ……
        private object ParseExpression() {
            object tc = Current;
            if (tc == Token.DOT || tc == Token.RPAREN) {
                throw new SyntaxException ("unexpected " + tc);
            } else if (tc == Token.LPAREN) {
                MoveNext();
                return ParseListBody();
            } else if (tc == Token.QUOTE) {
                MoveNext();
                return LL.List(LL.S_QUOTE, ParseExpression());
            } else if (tc == Token.TILDE) {
                MoveNext();
                return LL.List(LL.S_DELAY, ParseExpression());
            } else if (tc == Token.BQUOTE) {
                MoveNext();
                return QQ.Expand(ParseExpression());
            } else if (tc == Token.COMMA) {
                MoveNext();
                return new QQ.Unquote(ParseExpression());
            } else if (tc == Token.COMMA_AT) {
                MoveNext();
                return new QQ.UnquoteSplicing(ParseExpression());
            } else
                return tc;
        }
        ……
    } // Reader

このように構成した結果として,準引用式に普通のクォート記号を接頭したとき, クォートされたはずの準引用式が展開済みの式になる。 下記に対話セッションの例を示す。

> '`(cond ((numberp ,x) ,@y) (t (print ,x) ,@y))
(list 'cond (cons (list 'numberp x) y) (cons 't (cons (list 'print x) y)))
>

これは Emacs Lisp 等と異なる振舞だが,実用上は,問題を引き起こすよりも,むしろ 準引用式がどのように展開されるのかを確認する手頃な手段として役立つだろう。

4.2 静的クラス QQ

メソッド ParseExpression() が用いるクラス QQ.UnquoteQQ.UnquoteSplicing と静的メソッド QQ.Expand は, Lexer クラスや Reader クラスと同じ Lib_Reader.cs にある静的クラス QQ で定義される。

    /// <summary>準引用 (Quasi-Quotation)
    /// </summary>
    internal static class QQ
    {
        internal class Unquote
	……
        internal class UnquoteSplicing
	……
        /// <summary>準引用式 `x の x を等価な S 式に展開する
        /// </summary>
        internal static object Expand(object x) {
	……
    } // QQ

UnquoteUnquoteSplicing の定義は単純である。

        internal class Unquote
        {
            internal readonly object x;

            internal Unquote (object x) {
                this.x = x;
            }

            public override string ToString() {
                return "," + LL.Str(x);
            }
        } // Unquote
        internal class UnquoteSplicing
        {
            internal readonly object x;

            internal UnquoteSplicing (object x) {
                this.x  = x;
            }

            public override string ToString() {
                return ",@" + LL.Str(x);
            }
        } // UnquoteSplicing

ToString メソッドは,誤って準引用式の外で「,」や「,@」を使った場合や, デバッグ用に処理系内部の動作を Debug.WriteLine 等で可視化した場合の便宜のためにある。 通常は,処理系本体に準引用式を展開した後の式が渡されるため, UnquoteUnquoteSplicing のインスタンスが Lisp 式として出現することも,その ToString メソッドが使われることもない。

Unquote と UnquoteSplicing の名称は Scheme の対応する構文にちなんでいます。

準引用式の展開は Expand メソッドが再帰下降で行う。 このメソッドは「`」トークンの出現に対しそのトークンに続く任意の式 x を引数として呼び出される。 x には「,」や「,@」の出現に対して構築された UnquoteUnquoteSplicing のインスタンスが含まれ得る。

        /// <summary>準引用式 `x の x を等価な S 式に展開する
        /// </summary>
        internal static object Expand(object x) {
            if (x is Cell) {
                Cell t = Expand1(x);
                if (t.cdr == null) {
                    Cell k = t.car as Cell;
                    if (k != null)
                        if (k.car == LL.S_LIST || k.car == LL.S_CONS)
                            return k;
                }
                return new Cell(LL.S_APPEND, t);
            } else if (x is Unquote)
                return ((Unquote) x).x;
            else
                return Quote(x);
        }
        private static object Quote(object x) {
            if (x is Symbol || x is Arg || x is Cell)
                return LL.List(LL.S_QUOTE, x);
            else
                return x;
        }
        private static Cell Expand1(object x) {
            if (x is Cell) {
                Cell xc = (Cell) x;
                object h = Expand2(xc.car);
                object t = Expand1(xc.cdr);
                if (t is Cell) {
                    Cell tc = (Cell) t;
                    if (tc.car == null && tc.cdr == null)
                        return LL.List(h);
                    else if (h is Cell) {
                        Cell hc = (Cell) h;
                        if (hc.car == LL.S_LIST) {
                            if (tc.car is Cell) {
                                Cell tcar = (Cell) tc.car;
                                if (tcar.car == LL.S_LIST) {
                                    object hh = Concat(hc, tcar.cdr);
                                    return new Cell(hh, tc.cdr);
                                }
                            }
                            if (hc.cdr is Cell) {
                                object hh = ConsCons((Cell) hc.cdr, tc.car);
                                return new Cell(hh, tc.cdr);
                            }
                        }
                    }
                }
                return new Cell(h, t);
            } else if (x is Unquote)
                return LL.List(((Unquote) x).x);
            else
                return LL.List(Quote(x));
        }
        private static object Concat(Cell x, object y) {
            if (x == null)
                return y;
            else
                return new Cell(x.car, Concat((Cell) x.cdr, y));
        }
        private static object ConsCons(Cell x, object y) {
            if (x == null)
                return y;
            else
                return LL.List(LL.S_CONS, x.car, ConsCons((Cell) x.cdr, y));
        }
        private static object Expand2(object x) {
            if (x is Unquote)
                return LL.List(LL.S_LIST, ((Unquote) x).x);
            else if (x is UnquoteSplicing)
                return ((UnquoteSplicing) x).x;
            else
                return LL.List(LL.S_LIST, Expand(x));
        }

4.3 実装上の注意点

準引用式の基本的な展開方法は G. L. Steele Jr.: 'Common Lisp: The Language Second Edition', Digital Press, 1990, ISBN1-55558-041-6 いわゆる CLtL2 22.1.3 にある。 vector と「,.」を持たない本処理系に応用すると,それは次のようになる。

Ruby による準引用の実現で述べたように,この基本的な変換ルールに従っただけでは append を多用した効率の悪い式にしか展開できない。 本処理系では,再帰下降の各段階でそれぞれ式を簡単化している。 ただし,簡単化できなかった場合は,最後の手段として append の呼出しに展開する。 静的メソッド QQ.Expand に含まれる下記の文がそれに該当する。

                return new Cell(LL.S_APPEND, t);

しかし,本処理系では append は組込関数ではない。 そのかわり,2.1 節 で述べた初期化 Lisp スクリプト Prelude.l で次のように定義される。

(defun _append (x y)
  (if (null x)
      y
    (cons (car x) (_append (cdr x) y))))
(defmacro append (x &rest y)
  (if (null y)
      x
    `(_append ,x (append ,@y))))

この定義で使われている defmacrodefunifPrelude.lappend に先立って次のように定義される。

(setq defmacro
      (macro (name args &rest body)
             `(progn (setq ,name (macro ,args ,@body))
                     ',name)))
(defmacro defun (name args &rest body)
  `(progn (setq ,name (lambda ,args ,@body))
          ',name))
(defmacro if (test then &rest else)
  `(cond (,test ,then)
         ,@(cond (else `((t ,@else))))))

これに見るように Prelude.l ではマクロを定義するために準引用が多用されている。 マクロに準引用が多用されること自体は Lisp ではごく普通であり,驚くべきことではない。 ここで注意すべき点は,Prelude.lappend が定義されるより前に評価される準引用式がどれも append の呼出しへと展開されてはいない事実である。 展開時の簡単化がなければ,defundefmacro で定義した時点で append が未定義であるとしてエラーを起こしただろう。


目次へ 5.へ


Copyright (c) 2008 OKI Software Co., Ltd.