目次へ

3. イテレータによる字句解析器の構成と構文解析


主要な public クラスの見取り図を前章 2.2 節 から下記に再掲する。 ここでのクラスの色分けは,定義するソース・ファイルを示す。 赤が Lib_Interp.cs, 緑が Lib_Reader.cs, 青が Lib_Types.cs である。 前章前半で Main メソッドを含む Main.cs について, 後半で青の Lib_Types.cs について解説したように, 本章では主に緑の Lib_Reader.cs について解説する。


3.1 テキスト行の集合体 IEnumerable<string> と静的クラス Lines

言語処理系にとってソース・プログラムの読み取り処理は必要以上に具象化されるきらいがある。 例えば,Tiny Prolog in JavaJavaCC 4.0 経由で java.io.Reader に依存している。 funadoLispReader クラスは java.io.BufferedReader を内部に組み入れている。

java.io.BufferedReaderjava.io.Reader のためのデコレーションであり, その構築には抽象クラス java.io.Reader の実装を必要とする。 java.io.Reader は入出力クラスとしては簡素だが,言語処理系が要求する抽象度の水準とは一致しない。 (自由書式の言語であってもエラー報告のために必要な) 行の概念をそれ自身ではサポートしない一方で ready(), skip(long) などの不要な操作を定義している。

ライブラリとして処理系を利用する者にソース入力を任意に実装する自由を与えるため, 処理系の本体は,入出力に伴う細々とした操作がまとわりつかない抽象的なインタフェースに依存することが望ましい。 原理的には,行単位の文字列の無限に続き得る遅延リストがあれば十分である。

一見すると Java 1.5 以降の java.util.Iterator<String> がその用途に理想的であるように見える。 しかし,ガーベジコレクションの直接のトリガーにならない外部資源 (例えばファイルなど) を解放するメソッドをもたないため,そのままでは実際には使いづらい。

一方,C# でこれに対応する IEnumerator<string> は,現実の需要を賢明にとり入れ, 解放操作のための抽象的なインタフェース IDisposable を含んでいる。 つまり,string を含む任意の型 T に対して,

    public interface IEnumerator<T> : IDisposable, IEnumerator

である。 foreach 文など他の言語要素もこれと足並みをそろえている。 1行ごとに文字列を yield すると取り決めれば,IEnumerator<string> は言語処理系にとって適切なインタフェースになる。 したがって,本処理系では,続々・C# で作る Prolog 処理系 4. yield 文による外部イテレータの字句・構文解析への応用 と同じく,テキスト行の集合体として,ファイルや文字列からの入力を1行ごとに与える IEnumerator<string> オブジェクトを生成する IEnumerable<string> だけを扱うことにする。そして,その実装オブジェクトを作る関数を, Tiny Prolog in C# と同じく静的クラス Lines の静的メソッドとして用意する。 スクリプト・ファイルの名前とファイルの実体が入出力ライブラリを通じて結びつくのは,プログラム全体でここだけである。

    /// <summary>テキスト行の集合体を作る関数群</summary>
    /// <remarks>各テキスト行の行末に改行文字は含めない
    /// </remarks>
    public static class Lines
    {
        /// <summary>TextReader からの各行を与える</summary>
        /// <remarks>TextReader は終了時に Dispose される
        /// </remarks>
        public static IEnumerable<string> FromTextReader(TextReader tr) {
            try {
                for (;;) {
                    string line = tr.ReadLine();
                    if (line == null)
                        break;
                    yield return line;
                }
            } finally {
                tr.Dispose();
            }
        }

        /// <summary>ファイル名に対応するファイルの各行を与える
        /// </summary>
        public static IEnumerable<string> FromFile(string fileName) {
            return FromTextReader(new StreamReader (fileName));
        }

        /// <summary>文字列の各行を与える
        /// </summary>
        public static IEnumerable<string> FromString(string lines) {
            return FromTextReader(new StringReader (lines));
        }
    } // Lines

例えば fileName という名前のファイルの各行についての処理は,次のように書ける。

    foreach (string s in Lines.FromFile(fileName)) statement

(2.5 節の注で言及した C# Language Specification 3.0 の 8.8.4 節によれば) この foreach 文は次と等価である。

    {
        IEnumerator<string> e = Lines.FromFile(fileName).GetEnumerator();
        try {
            string s;
            while (e.MoveNext()) {
                s = e.Current;
                statement
            }
        } finally {
            e.Dispose();
        }
    }

(C# Language Specification 3.0 の 10.14.4.3 節によれば) e.Dispose() により,Lines.FromTextReader のメソッド本体の finally 節にある tr.Dispose() が実行される。 したがって,foreach 文が正常終了したときも,例外発生により中止されたときも,ファイル読み取り用に内部で作られた StreamReader オブジェクトが Dispose され,外部資源が解放される。

3.1.1 IronPython による実験

Lines.FromString で文字列が行ごとに分割されて yield されることを確かめる。

IronPython 1.1.1 (1.1.1) on .NET 2.0.50727.1433
Copyright (c) Microsoft Corporation. All rights reserved.
>>> import clr
>>> clr.AddReferenceToFileAndPath("C:/l2lisp-8.2/L2LispLib.dll")
>>> from L2Lisp import *
>>> for s in Lines.FromString("ab\nc\ndef"): print "-", s
...
- ab
- c
- def
>>>

C# のクラス・ライブラリを利用するため,C# の foreach 文と同じように IronPython の for 文も最後に解放処理を行う。 TextReader オブジェクトの Dispose メソッドが最後に呼び出されることを確かめる。

>>> from System.IO import TextReader
>>> class DummyReader (TextReader):
...   def __init__(self):
...     self.count = 0
...   def ReadLine(self):
...     if self.count == 5: return None
...     self.count += 1
...     return str(self.count)
...   def Dispose(self, *a):
...     print "bye", a
...
>>> for s in Lines.FromTextReader(DummyReader()): print "-", s
...
- 1
- 2
- 3
- 4
- 5
bye (True,)
>>>

この最後の出力は DummyReader インスタンスに対して Dispose() ではなく Dispose(true) が呼び出されたことを意味する。 DummyReader の基底クラス TextReader は,無引数の Dispose をオーバーライド不可の public void Dispose() として定義し, 1引数の Dispose を派生クラス用に protected virtual void Dispose(bool) と定義している。 したがって,Lines.FromTextReader(tr)finally 節にある tr.Dispose() では,仮引数 tr のコンパイル時型にしたがって TextReader クラスの Dispose() が呼び出される。 そして,その内部で this の実行時型にしたがって DummyReader クラスの Dispose メソッドが true を引数として呼び出されたわけである。

3.2 IEnumerable<string> 実装としての InteractiveInput

対話的なコンソール入力のため,Python による実装 L2 Lisp 7.2object の派生クラスとして InteractiveInput をはじめて導入した。 C# による実装はこれを IEnumerable<string> の1実装クラスとして定義する。 L2 Lisp 7.2 と比べ操作そのものにはほとんど変更はないが,C# では静的な型システムに厳密にあてはめる必要がある。

    /// <summary>プロンプト付きコンソール入力
    /// </summary>
    public class InteractiveInput: IEnumerable<string>
    {
        private readonly string ps1;
        private readonly string ps2;
        private bool primary;

        /// <param name="ps1">1次プロンプト</param>
        /// <param name="ps2">2次プロンプト
        /// </param>
        public InteractiveInput (string ps1, string ps2) {
            this.ps1 = ps1;
            this.ps2 = ps2;
            primary = true;
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        /// <summary>
        /// 最初は1次プロンプト,次回以降は2次プロンプトを表示して
        /// 1行を入力し,その行を yield する
        /// </summary>
        public IEnumerator<string> GetEnumerator() {
            for (;;) {
                if (primary) {
                    primary = false;
                    Console.Write(ps1);
                } else {
                    Console.Write(ps2);
                }
                string line = Console.In.ReadLine();
                if (line == null)
                    break;
                yield return line;
            }
        }

        /// <summary>1次プロンプトに戻す
        /// </summary>
        public void Reset() {
            primary = true;
        }
    } // InteractiveInput

IEnumerable<string> に対して void Reset() メソッドを追加定義している点に注意されたい。 Lisp 式の対話入力が1個完成したとき,次に表示するプロンプトを1次プロンプトに戻すために Reset を呼び出す必要がある。

最初に Python による実装に InteractiveInput を導入した動機は GNU readline による行編集でした。 C# にそういったものは (標準では) 存在しませんから,Lines.FromConsole() と同じく,単に Console.In.ReadLine() の呼出しをしています。 しかし,Mono はともかく Windows 上の .NET Framework では,このメソッドでも入力時の行編集が可能です。

3.3 文字イテレータとトークン・イテレータ

与えられた IEnumerable<string> オブジェクト lines から得られる各行の各文字を一つずつ yield する 文字イテレータCharsFrom(lines) として定義する。 エラーを報告するときのために,次の行に進むごとに現在行 currentLine と現在の行番号 currentLineNumber を更新する。 外側の foreach ループが終了してすべての行が尽きたとき (このとき,前述の Dispose により,読み取っているファイルが解放される),EOF (End Of File) かどうかのフラグ isEOF を真にする。

        private IEnumerable<char> CharsFrom(IEnumerable<string> lines) {
            try {
                foreach (string s in lines) {
                    currentLine = s;
                    currentLineNumber++;
                    foreach (char c in s)
                        yield return c;
                    yield return '\n';
                }
            } finally {
                isEOF = true;
            }
        }

この CharsFrom(lines) を次のような 字句解析器 クラス Lexer のメソッドとする。 LexerGetEnumerator() は字句解析の結果としてのトークンを次々と yield するトークン・イテレータである。

    internal class Lexer: IEnumerable<object>
    {
        private readonly IEnumerable<string> lines;
        private int currentLineNumber = 0;
        private string currentLine = string.Empty;
        private bool isEOF = false;

        internal Lexer (IEnumerable<string> lines) {
            this.lines = lines;
        }

        internal int LineNumber {
            get { return currentLineNumber; }
        }

        internal string Line {
            get { return currentLine; }
        }

        internal void Reset() {
            InteractiveInput ii = lines as InteractiveInput;
            if (ii != null)
                ii.Reset();
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        public IEnumerator<object> GetEnumerator() {
            using (IEnumerator<char> ch = CharsFrom(lines).GetEnumerator()) {
                ch.MoveNext();
                for (;;) {
                    while (! isEOF && Char.IsWhiteSpace(ch.Current))
                        ch.MoveNext();
                    if (isEOF)
                        break;
                    char cc = ch.Current;
                    if (cc == ';') { // ; コメント
                        while (ch.MoveNext() && ch.Current != '\n') {}
                        if (isEOF)
                            break;
                    } 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.LPAREN;
                    …略…
                    }
                    ch.MoveNext();
                }
            }
        }
    
        …略…
    } // Lexer

ここでは foreach 文を使わずに,直接 CharsFrom(lines) の戻り値に対し GetEnumerator を呼び出している。 そして,その戻り値である IEnumerator<char> オブジェクトを ch に格納している。 この操作を using 文で囲んでいるから, 妥当に使用される限り,EOF まで読み進んだときも,例外で中止されたときも,ch に対する Dispose が適切に行われ,(もしあれば) 読み取っているファイルが解放される。

ここで Token.COMMA_ATToken.COMMA は,次のように Lib_Reader.cs で定義されているシンボルである。

    internal static class Token {
        internal static readonly Symbol BQUOTE = Symbol.Of("`");
        internal static readonly Symbol COMMA = Symbol.Of(",");
        internal static readonly Symbol COMMA_AT = Symbol.Of(",@");
        internal static readonly Symbol DOT = Symbol.Of(".");
        internal static readonly Symbol LPAREN = Symbol.Of("(");
        internal static readonly Symbol RPAREN = Symbol.Of(")");
        internal static readonly Symbol QUOTE = Symbol.Of("'");
        internal static readonly Symbol TILDE = Symbol.Of("~");
    } // Token
Symbol インスタンスはいったん作られると内容を変更できません。 readonly メンバは,どれを参照するかを変更できません。 したがって Token.COMMA_AT は事実上,定数のように振舞います。 しかし,本来,C# の標準の命名規則では,たとえ定数であっても,大文字で書いて下線でつなげる名前にしてはいけません。 ここでそのような名前にしているのは,Python による実装から翻訳した名残です。 public ではなく internal と宣言することによって,.NET Framework に対する名前の邪悪さをアセンブリ内だけに封じています。

トークン・イテレータ内で IEnumerator<char> オブジェクトを直接扱うことには次の利点がある。

  1. トークンを切り出すとき,文字の先読みが必要な場合と不要な場合がある。 数やシンボルを切り出すときは,次の文字が数やシンボルの一部として妥当かどうかを判定するために先読みが必要である。 一方,(Lisp では) 丸括弧はそれだけで一つのトークンだから,次の文字を読む必要は無い。 前者を yield するときは,既に先読みが済んでいるから単に yield return token をし, 後者を yield するときは,まだ先読みしていないから yield return token; ch.MoveNext() をすればよい。
  2. 言語仕様の制約として yield そのものはイテレータでしかできないが, IEnumerator<char> オブジェクトを引数として下請けメソッドに渡すことにより,トークンの切り出し処理を分割できる。

つまり,従来の字句解析器の構成で次の1文字を読み取る手続きと先読みした文字を保持する変数の2者を統合したものとして IEnumerator<char> オブジェクトを扱うことができる。 逆にいえば,C# のイテレータとは,そういった手続きと変数をシステマティックに構成する手段とみることができる。

下記は,文字列のエスケープ列を解釈するとき,'0' 以上 '7' 以下の ch.Current に対して高々3桁の8進数を読み取り,1個の char 値を返す下請けメソッド (正確には,イテレータからみて GetString の下にある孫請けメソッド) である。

        private static char GetOctChar(IEnumerator<char> ch) {
            int result = ch.Current - '0';
            for (int i = 0; ch.MoveNext() && i < 2; i++) {
                char cc = ch.Current;
                if ('0' <= cc && cc <= '9')
                    result = (result << 3) + cc - '0';
                else
                    break;
            }
            return (char) result;
        }
行,文字,トークンとイテレータを3重に利用した字句解析器の構成は,Tiny Prolog in C# の成果を応用したものです。 L2 Lisp の Ruby への最初の移植 以来,字句解析の方法として, 正規表現で最初にトークンをおおまかに切り出すという簡便な方法をとってきました。 今回は,主に文字列のエスケープ列の正確な処理を実現するため, 字句解析の方法として 標準 Pascal による最後の実装 以来,約1年ぶりにテキストの先頭から切り出していくという正攻法をとりました。 ただし,せっかく C# にはイテレータという便利な道具があるわけですから,ここではそれを利用したわけです。

もちろん,対応する有限オートマトンがあるような普通の字句規則を採用する限り,原理的には, 字句解析器と完全に等価な正規表現を導出できます。 これまでの簡便な方法に基本的な限界があるわけではありません。

C# のイテレータは Python のジェネレータと同じく,いわゆる Ruby 用語での外部イテレータに相当します (人間が書くプログラム・コードは,見かけ上,内部イテレータと同じになりますが)。 Tiny Prolog in C# の当時と異なり,最近の Ruby は C# や Python と同じように外部イテレータも容易に扱えるようになった模様ですから, 字句解析器を構成する方法として今なら本処理系の方式を Python だけでなく Ruby にも等しく応用できるかもしれません。

3.4 Lisp 式のための Reader

Reader クラスは,Lisp 式を読み取るためにインタープリタ・クラス Interp が直接使う 構文解析器 クラスである。 Reader のコンストラクタは,テキスト行の集合体を抽象化した IEnumerable<string> 型の lines を引数としてとる。 いわゆるファイルにとらわれない広汎な入力ソースを与えることができる。 コンストラクタは,与えられた lines からトークンを次々にとりだす IEnumerator<object> オブジェクト tk を構成する。

Reader インスタンスを (using 文などによって) Dispose したとき, コンストラクタで構成されたトークン列挙子 tkDispose される。 これにより,(もしあれば) lines の背後にあるファイルその他の外部資源が解放される。

    /// <summary>Lisp 式を読む
    /// </summary>
    public class Reader: IDisposable
    {
        private readonly Lexer lex;
        private IEnumerator<object> tk;
        private bool erred = false;
        private bool hasCurrent = true;

        /// <summary>EOF を表すシンボル
        /// </summary>
        public static readonly Symbol EOF = LL.S_EOF;

        /// <param name="lines">ここから行単位に文字列を読み取る
        /// </param>
        public Reader (IEnumerable<string> lines) {
            lex = new Lexer (lines);
            tk = lex.GetEnumerator();
        }

        /// <summary>もしあればトークン列挙子を解放する
        /// </summary>
        public virtual void Dispose() {
            if (tk != null) {
                tk.Dispose();
                tk = null;
            }
        }
このクラスの Dispose() メソッドは,メンバ tk の Dispose() メソッドを呼び出すだけですから,簡単な作りにしています。 万一 Dispose() し損なったとき,このクラスは何もしませんが,tk を経由して参照されるどれかのクラスのデストラクタが最後の手段としてガーベジコレクション時に資源を解放します。 このような簡単な作りは,将来にわたってこのクラスやその派生クラスで外部資源を じかに解放することがない,と確信できるときだけ妥当です。
もしも外部資源をじかに解放する場合は,デストラクタを定義し,厳密に1回は,そして1回だけ解放するために, protected virtual void Dispose(bool) を使った一定のパターンで non-virtual な public void Dispose() メソッドと協調させます。

ここで LL.S_EOF は,次のように Lib_Types.cs の静的クラス LL で定義されているシンボルの一つである。

        internal static readonly Symbol
            S_APPEND = Symbol.Of("append"),
            S_CATCH = Symbol.Of("catch"),
            S_COND = Symbol.Of("cond"),
            S_CONS = Symbol.Of("cons"),
            S_DELAY = Symbol.Of("delay"),
            S_EOF = Symbol.Of("#<eof>"),
            S_ERROR = Symbol.Of("*error*"),
            S_LAMBDA = Symbol.Of("lambda"),
            S_LIST = Symbol.Of("list"),
            S_MACRO = Symbol.Of("macro"),
            S_PROGN = Symbol.Of("progn"),
            S_QUOTE = Symbol.Of("quote"),
            S_REST = Symbol.Of("&rest"),
            S_SETQ = Symbol.Of("setq"),
            S_T = Symbol.Of("t"),
            S_UNWIND_PROTECT = Symbol.Of("unwind-protect");

Read() は Reader クラスの最も重要な public メソッドである。 このメソッドを機能させるために,ソース・ファイル Lib_Reader.cs のすべてがあるといってもよい。 Read() はコンストラクタで与えられたテキスト行の集合体から順に1個の Lisp 式を読取り,2.3 節 で述べたように object として返す。

エラーを報告するときのために Lexer クラスの CharsFrom(lines) イテレータでセットしておいた現在行 Line とその行番号 LineNumber は,SyntaxExceptioncatch したとき,評価時例外 EvalException のメッセージを組み立てるために参照される。

        /// <summary>1個の Lisp 式を読む</summary>
        /// <remarks>行が尽きたら EOF を表すシンボルをいつまでも返す
        /// </remarks>
        public object Read() {
            lex.Reset();
            if (erred) { // 前回が構文エラーだったなら次の行まで読み飛ばす
                int n = lex.LineNumber;
                while (hasCurrent && lex.LineNumber == n)
                    MoveNext();
                erred = false;
            } else {
                if (hasCurrent)
                    MoveNext();
            }
            if (hasCurrent)
                try {
                    return ParseExpression();
                } catch (SyntaxException ex) {
                    erred = true;
                    throw new EvalException ("SyntaxError: " + ex.Message +
                                             " -- " + lex.LineNumber +
                                             ": " + lex.Line);
                }
            else
                return EOF;
        }

ここで MoveNext() を次のように定義する。 現在のトークンを表すプロパティ Current も定義する。 Current は基本的には tk.Current の値を返す。 ただし,字句エラーのとき tk.CurrentSyntaxException 値をとる。 そのときは,値として返すかわりに例外として送出する。 トークンが尽きてなおプロパティが参照されたときは,予期しない EOF (unexpected EOF) に対する例外を送出する。

        private void MoveNext() {
            hasCurrent = tk.MoveNext();
        }

        private object Current {
            get {
                if (hasCurrent) {
                    object tc = tk.Current;
                    SyntaxException ex = tc as SyntaxException;
                    if (ex != null)
                        throw ex;
                    return tc;
                } else
                    throw new SyntaxException ("unexpected EOF");
            }
        }

Lisp の簡素な構文構造を反映して,式 (expression) の構文解析 (parse) は単純である。 Token.LPAREN つまり左括弧 (left parenthesis) が出現したときは,その次のトークンを MoveNext() で読み込んでからリストの本体 (list body) を再帰下降的に構文解析する。

Token.QUOTE つまりシングル・クォートが出現したときは, その次のトークンを MoveNext() で読み込んでから再帰的に式を一つ読み取る。 そして,LL.S_QUOTE つまりシンボル quote と組み合わせて LL.List 関数で長さ2のリストを構成する。 これにより,例えば“'(+ 2 3)”というテキストから (quote (+ 2 3)) というリストが得られる。

L2 Lisp 4.2 以来導入されたチルダ記法に対しても同様に処理する。 それにより,例えば“~(+ 2 3)”というテキストから (delay (+ 2 3)) というリストが得られる。

準引用式を構成する Token.BQUOTEToken.COMMAToken.COMMA_AT については後述する。

        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;
        }
        private object ParseListBody() {
            if (Current == Token.RPAREN) {
                return null;
            } else {
                object e1 = ParseExpression();
                MoveNext();
                if (Current == Token.DOT) {
                    MoveNext();
                    object e2 = ParseExpression();
                    MoveNext();
                    if (Current != Token.RPAREN)
                        throw new SyntaxException ("\")\" expected: "
                                                   + Current);
                    return new Cell (e1, e2);
                } else {
                    object tail = ParseListBody();
                    return new Cell (e1, tail);
                }
            }
        }
Read というメソッド名は,Lisp 関数 read と直接関係しています。 実際,Read メソッドは read 関数の実装として使われます。 2.3 節 で述べた組込み無引数 Lisp 関数の型 NullaryFunction に Read メソッドが適合することは偶然ではありません。 構文解析器クラスなのにクラス名が Parser ではなく Reader なのは,このクラスの責務が要するに read の実装だからです。

3.4.1 IronPython による実験

四つの Lisp 式を含む文字列を与える。

>>> r = Reader(Lines.FromString("1 (+ 2 3) '(a b (c d)) ~(+ 4 5)"))
>>> r.Read()
1
>>> r.Read()
<L2Lisp.Cell object at 0x000000000000002B [(+ 2 3)]>
>>> r.Read()
<L2Lisp.Cell object at 0x000000000000002C ['(a b (c d))]>
>>> r.Read()
<L2Lisp.Cell object at 0x000000000000002D [(delay (+ 4 5))]>
>>> r.Read()
<L2Lisp.Symbol object at 0x000000000000002E [#<eof>]>
>>> 

ここで '(a b (c d)) に対して <L2Lisp.Cell object at 0x000000000000002C [(quote (a b (c d)))]> でなく <L2Lisp.Cell object at 0x000000000000002C ['(a b (c d))]> と表示されているが,これは式を文字列化する LL.Str(object) で,quote で始まる2要素のリストを特別扱いをしているからである。 下記に例を示す。

>>> q = Symbol.Of("quote")
>>> lst = LL.List
>>> s = LL.Str
>>> s(lst(q))
'(quote)'
>>> s(lst(q, lst(1, 2, 3)))
"'(1 2 3)"
>>> s(lst(q, lst(1, 2, 3), 4))
'(quote (1 2 3) 4)'
>>> 


目次へ 4.へ


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