目次へ

6. 組込関数の定義


本章は Lisp 組込関数を定義するためのクラスとメソッドについて説明する。

本処理系で Lisp 組込関数 (以下,紛らわしくない限り,単に Lisp 関数) を定義するには,2.1 節 で述べたように,関数を実装する公開メソッドにカスタム属性 LispFunction を与え,その公開メソッドをもつオブジェクトに対し,インタープリタの LoadNatives メソッドを適用すればよい。 Main.cs の静的メソッド Main の関係する部分を示す。

        Interp interp = new Interp();
        interp.LoadNatives(interp);

インタープリタ・クラス Interp の定義のうち Lisp 関数としてロードされるメソッドは,ファイル Lib_Funcs.cs におさめた。

    public partial class Interp
    {
        // ここでは Lisp 関数を定義する。ただし,
        // (read) と (list ...) は Interp のコンストラクタで登録済みである

        /// <summary>(car (cons x y)) =&gt;x</summary>
        [LispFunction("car")]
        public static object Car(object list) {
            return (list == null) ? null : ((Cell) list).car;
        }

        /// <summary>(cdr (cons x y)) =&gt; y</summary>
        [LispFunction("cdr")]
        public static object Cdr(object list) {
            return (list == null) ? null : ((Cell) list).cdr;
        }

        ……
    } // Interp

それ以外の Interp の定義は Lib_Interp.cs におさめた (5 章)。 この分割は C# の仕様による強制ではない。 単にソース・ファイルを人間にとって手頃な大きさにするための,自由裁量による分割である。 コードのコメントにある Interp コンストラクタでの Lisp 関数 (read)(list ...) の登録については 5.5 節で述べた。

6.1 カスタム属性 LispFunction

カスタム属性 LispFunction の実装クラス LispFunctionAttribute を下記のように Lib_Types.cs に定義する。 Attribute の派生クラスとし,Microsoft FxCop 1.35 の指示に従い sealed class とする。 メソッドに対する LispFunction 属性指定の有無の判定において,sealed 修飾により,LispFunctionAttribute 以外の属性クラスを安全に無視できるから,.NET 標準クラス・ライブラリ内部でのリフレクション演算の効率向上が期待される。

    /// <summary>組込みの Lisp 関数 であることを示す属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public sealed class LispFunctionAttribute:  Attribute
    {
        private string name;
        private bool isLazy;

        /// <param name="name">Lisp としての関数名</param>
        /// <param name="isLazy">真ならば,引数が約束のときそれを評価しない
        /// </param>
        public LispFunctionAttribute (string name, bool isLazy) {
            this.name = name;
            this.isLazy = isLazy;
        }

        /// <summary>LispFunctionAttribute(name, false) と同義
        /// </summary>
        public LispFunctionAttribute (string name)
            : this (name, false) {}

        /// <summary>Lisp としての関数名
        /// </summary>
        public string Name {
            get { return name; }
        }

        /// <summary>真ならば,引数が約束のときそれを評価しない
        /// </summary>
        public bool IsLazy {
            get { return isLazy; }
        }
    } // LispFunctionAttribute
派生クラスを定義できない「封印されたクラス」 "sealed class" という用語を採った言語は,私が知る限りでは,かつて Apple 社が開発していた Dylan が最初です。 括弧だらけでない伝統的な親しみやすい表記方法と,Scheme 風の健全なマクロを両立させた,このまま歴史の闇に消えるには惜しい強力なオブジェクト指向言語です。
A. Shalit: "The Dylan Reference Manual", Addison-Wesley, 1996, ISBN 0-201-44211-6

属性クラスへの [AttributeUsage(AttributeTargets.Method)] の指定は,属性の適用対象 (target) をメソッド (method) だけに制限する。 これにより,誤ってメソッド以外に LispFunction 属性を与えたとき,コンパイル時にエラーとして報告される。

属性クラス LispFunctionAttribute の1引数コンストラクタ

        public LispFunctionAttribute (string name)
            : this (name, false) {}

は,前節で示した Lisp 関数 car の実装メソッド

        [LispFunction("car")]
        public static object Car(object list) {
            return (list == null) ? null : ((Cell) list).car;
        }

にあるような1引数 (ここでは "car") を伴う属性指定に対して呼び出される。 このことから予想されるとおり,[LispFunction("car")] という指定を [LispFunction("car", false)] と書いてもよい。 その場合は,2引数コンストラクタ

        public LispFunctionAttribute (string name, bool isLazy) {
            this.name = name;
            this.isLazy = isLazy;
        }

が適用される。 コンストラクタの仮引数 name に対する実引数 "car" は Lisp 関数の名称として使われる (これに対し,C# のメソッド名は Lisp 関数名とは無関係であり,どのような名前でもよい。 この例では「Car」というメソッド名にしているが, 「LispFunctionCar」でも「GetFirstElementOfTheList」でもよい)。 isLazy に対する false は Lisp 関数の引数が遅延評価の約束だったとき,それを評価することを指示する。 ほとんどの場合,false を与えるのが妥当である。

その一方,引数が約束だったとき,それを評価しない例として cons がある。 属性指定の第2引数として false ではなく true を与えていることに注意されたい。

        [LispFunction("cons", true)]
        public static object Cons(object car, object cdr) {
            return new Cell (car, cdr);
        }

Lisp 式の引数として約束を与えたときの carcdrcons の違いを下記の例に示す。 carcdr と異なり,cons が結果の値を構築するときには,その引数の具体的な値を得る必要はない。 遅延リスト構築の背景には cons のこの性質がある。

> (car ~(cons 1 2))
1
> (cdr ~(cons 1 2))
2
> (cons ~(cons 1 2) ~(cons 2 3))
(#<promise:25d17700> . #<promise:d5c91f40>)
> 
5.5 節 で述べたように,約束に対する性質については,list 関数も cons と同じです。

Lisp 関数の実装メソッドは必ずしも静的メソッドに限られない。 ファイル名の文字列を引数にとり,Lisp スクリプトとして実行する (load fileName) の実装メソッドは次のように定義される。

        /// <summary>(load fileName) =&gt; ファイルを Lisp プログラムとして評価
        /// </summary>
        [LispFunction("load")]
        public object Load(object fileName) {
            return Run(Lines.FromFile((string) fileName));
        }

Interp クラスの非静的メソッド Run (5.1 節) を呼び出すためには,このメソッド自身が非静的でなければならない。 Lisp 関数 evalapply などの実装メソッドについても同様である。 そして,このことが,ほかならない Interp クラスに Lisp 関数の実装メソッドを定義する理由である。

もしもほかのクラスで Lisp 関数の実装メソッドを定義し,そこから Interp クラスのメソッドを呼び出すならば,典型的には,Lisp プログラムでその関数の引数として陽に (cs-self) の値を与える (あるいはラムダ式でそのようなクロージャを構成する) 必要がある。

        /// <summary>(cs-self) =&gt; インタープリタ自身のオブジェクト
        /// </summary>
        [LispFunction("cs-self")]
        public object CsSelf() {
            return this;
        }

ところで,(load fileName) の実装メソッドで仮引数 fileName の型を string ではなく,あいまいに object と宣言したのは,本来は不適切である。しかし,現在の処理系実装の制限のため,そうせざるを得ない。 正確には,実装メソッドは 2.3 節 で述べた下記のいずれかと適合しなければならない。

    /// <summary> 組込みの 0 引数 Lisp 関数の型
    /// </summary>
    public delegate object NullaryFunction ();

    /// <summary> 組込みの 1 引数 Lisp 関数の型
    /// </summary>
    public delegate object UnaryFunction (object arg);

    /// <summary> 組込みの 2 引数 Lisp 関数の型
    /// </summary>
    public delegate object BinaryFunction (object x, object y);

    /// <summary> 組込みの N 引数 Lisp 関数の型
    /// </summary>
    public delegate object NAryFunction (IList args);

この制限の直接の理由は,インタープリタへの関数のロードを行う LoadNatives メソッドの実装にある。

6.2 インタープリタへの組込関数のロード

本処理系は,インタープリタへの Lisp 組込関数のロードを行う InterpLoadNatives メソッドを,二つの公開メソッドとして用意する。 一つは型オブジェクトを引数にとり,もう一つは (なんらかの型のインスタンスである) 任意のオブジェクトを引数にとる。

        /// <summary>
        /// クラスで定義されている Lisp 関数をロードする
        /// </summary>
        /// <remarks>
        /// 引数が表すクラスから LispFunction 属性付き public メソッドを探す。
        /// これらは static なメソッドでなければならない
        /// </remarks>
        public void LoadNatives(Type type) {
            LoadNatives(type, null);
        }

前述の Main.cs で使われたのは,任意のオブジェクトを引数にとる下記のメソッドである。

        /// <summary>
        /// 引数が属するクラスで定義されている Lisp 関数をロードする
        /// </summary>
        /// <remarks>
        /// 引数のクラスから LispFunction 属性付き public メソッドを探す。
        /// インスタンスは非 static なメソッドの this として使われる
        /// </remarks>
        public void LoadNatives(object target) {
            if (target == null)
                throw new ArgumentException ("null target");
            LoadNatives(target.GetType(), target);
        }

下記の非公開メソッドが二つの公開メソッドを実質的に実装する。

        private void LoadNatives(Type type, object target) {
            Type ATTR = typeof (LispFunctionAttribute);
            foreach (MethodInfo method in type.GetMethods()) {
                Attribute attr = Attribute.GetCustomAttribute(method, ATTR);
                if (attr != null) {
                    string name = ((LispFunctionAttribute) attr).Name;
                    bool is_lazy = ((LispFunctionAttribute) attr).IsLazy;
                    ParameterInfo[] pi = method.GetParameters();
                    int arity = pi.Length;
                    Type dt;
                    if (arity == 0) 
                        dt = typeof (NullaryFunction);
                    else if (arity == 1)
                        if (pi[0].ParameterType == typeof (IList))
                            dt = typeof (NAryFunction);
                        else
                            dt = typeof (UnaryFunction);
                    else if (arity == 2)
                        dt = typeof (BinaryFunction);
                    else
                        throw new ArgumentException ("invalid arity");
                    Delegate d;
                    if (method.IsStatic) {
                        d = Delegate.CreateDelegate(dt, method);
                    } else {
                        if (target == null)
                            throw new ArgumentException
                                ("null target for non-static method");
                        d = Delegate.CreateDelegate(dt, target, method);
                    }
                    symbol[Symbol.Of(name)] = d;
                    if (is_lazy)
                        lazy[d] = true;
                }
            }
        }

型オブジェクト引数 type に対し,type.GetMethods() で公開メソッドの情報を得る。 その各要素 method に対し,クラス Attribute の静的メソッド GetAttribute を,第2引数に LispFunctionAttribute の型オブジェクトを指定して呼び出す。 その戻り値 attr は, もしも method が表すメソッドに LispFunction 属性が指定されていれば,非 nullLispFunctionAttribute インスタンスになる。

LispFunctionAttribute インスタンスとして attr から Lisp 関数としての名称 name と,約束を評価しないかどうかのフラグ is_lazy を得る。 method.GetParameters() からメソッドの仮引数の情報を得る。 ただし,仮引数の情報のうち実際に使うのは引数の個数 arity だけである。 arity に該当する (2.3 節 で述べた) 定義済みのデリゲートの型オブジェクト dt を選出する。

dtmethod,そして引数 target から Delegate.CreateDelegate によってデリゲート・インスタンス d を得る。 このとき,method が静的メソッドを表すかどうかで target を使わないか使うかを場合分けする。

name に該当するシンボルをキーとして,インタープリタのシンボル・テーブル symbold を格納する。 これは name という名前の大域変数を,変数値を d として定義することに等しい。 もしもフラグ is_lazy が立っていれば,約束を評価しない関数の集合 lazy の1要素として d を追加する。

こうしてロードされた関数の Lisp 式での適用については 5.5 節 で述べた。

6.3 新しい組込関数の定義例

利用者が独自に新しく組込関数を定義する方法の手本として,初期化 Lisp スクリプト Prelude.l の末尾に下記のような Lisp コードをおいた。

(when (equal (cadr *version*) "C#")
  (cs-load (cs-path-to "PreludeExtra.dll") "Extra"))

このコードは,大域変数 *version* を調べて,今実行している Lisp が C# による実装かどうか確かめる。 そして,もしもそうならば,PreludeExtra.dllExtra クラスから Lisp 関数をロードする。

(cs-path-to fileName) は,実行ファイルと同じフォルダにある fileName という名前のファイルのパス名を返す。 手本では L2Lisp.exe と同じフォルダにある PreludeExtra.dll のパス名を返している。 もちろん,利用者が独自に用意した DLL ファイルを使うときは,必ずしも cs-path-to を使わず,アクセス可能な任意のパス名を直接 cs-load に与えてよい。

(cs-load dllName className) は,dllName という名前の DLL ファイルをロードし,そこに定義されている className という名前のクラスから Lisp 関数をロードする。 Lib_Funcs.cs にある実装メソッドを示す。

        /// <summary>(cs-load dllName className)</summary>
        /// <remarks>
        /// dllName の DLL をロードし,その className のクラスに含まれる
        /// LispFunction 属性の public メソッドを Lisp 関数として定義する
        /// </remarks>
        [LispFunction("cs-load")]
        public object CsLoadNatives(object dllName, object className) {
            Assembly asm = Assembly.LoadFile((string) dllName);
            Type typ = asm.GetType((string) className);
            LoadNatives(typ);
            return null;
        }

PreludeExtra.dll のソースである PreludeExtra.cs では,Extra クラスを internal として定義した。 DLL ファイルをロードして型オブジェクトを得るためだけならば,クラス自身が public である必要はない。

using System;
using System.Collections;
using System.Reflection;
using L2Lisp;

[assembly: AssemblyTitle("An example of defining Lisp functions in C#")]
[assembly: AssemblyCopyright
 ("Copyright (c) 2007, 2008 OKI Software Co., Ltd.")]
[assembly: AssemblyDescription
 ("This is distributed under the MIT/X11 license.  See Copyright.txt.")]

/// <summary>C# による Lisp 関数の利用者定義の例 (Prelude.l を参照)
/// </summary>
internal class Extra
{
ここではファイルを1本にまとめるために,アセンブリ属性も同じファイルで指定しています。 もちろん,これは,こうしなければならないわけではありません。むしろ非模範的な方法でしょう。 Microsoft Visual Studio を利用した場合,アセンブリ属性の指定をまとめた AssemblyInfo.cs が自動的に用意されます。

Extra では手本として二つの Lisp 関数を実装した。一つはインタープリタを終了させる exit 関数であり,もう一つは C や Ruby でおなじみの printf 関数 (ただし簡易版) である。 どちらも Python/Ruby による実装では PRELUDE 内での動的メソッド呼出しで実装されていた関数である。

exit 関数を実装するには静的メソッド Environment.Exit を使えばよい。

    /// <summary>(exit [code])</summary>
    [LispFunction("exit")]
    public static object Exit(IList args) {
        int n = args.Count;
        if (n == 0)
            Environment.Exit(0);
        else if (n == 1)
            Environment.Exit((int) args[0]);
        else
            throw new EvalException ("(exit) or (exit code) expected");
        return null;
    }

(printf format args...) は,書式文字列 format にしたがって args... に対する文字列を静的メソッド Console.Write で印字する。

可能な変換指定子は dxXs%pr である。 整数に対する dxX は 0 フラグと最小フィールド幅を指定できる。 Ruby/Python と同じく s に対する実引数は文字列に限られない。 s は実引数に対し,標準の文字列化メソッドである ToString() を呼び出す。 pr はそれぞれ Ruby と Python の同名の変換指定子に由来し,実引数を分かりやすく (Lisp の prin1 関数のように) 印字する。

    /// <summary>(printf format arg...)</summary>
    /// <remarks>極超簡易版 (つまり手抜き) の printf 実装
    /// </remarks>
    [LispFunction("printf")]
    public static object PrintF(IList args) {
        string format = (String) args[0];
        IEnumerator er = args.GetEnumerator();
        er.MoveNext();
        PrintF1(format, er);
        return null;
    }
    private static void PrintF1(string format, IEnumerator er) {
        int n = format.Length;
        for (int i = 0; i < n; i++) {
            char c = format[i];
            if (c != '%') {
                Console.Write(c);
                continue;
            }
            c = format[++i];
            bool zero = false;
            int width = 0;
            if (c == '0') {
                zero = true;
                c = format[++i];
            }
            if ('1' <= c && c <= '9') {
                width = c - '0';
                c = format[++i];
                if ('0' <= c && c <= '9') {
                    width = width * 10 + c - '0';
                    c = format[++i];
                }
            }
            if (c == '%') {
                Console.Write('%');
            } else if (c == 'd' || c == 'x' || c == 'X') {
                if (! er.MoveNext())
                    goto TOO_FEW_ARGS;
                int j = (int) er.Current;
                PrintInteger(j, zero, width, c);
            } else if (c == 'p' || c == 'r') { // Ruby || Python
                if (! er.MoveNext())
                    goto TOO_FEW_ARGS;
                string j = LL.Str(er.Current);
                Console.Write(j);
            } else if (c == 's') {
                if (! er.MoveNext())
                    goto TOO_FEW_ARGS;
                string j = er.Current.ToString();
                Console.Write(j);
            } else {
                throw new EvalException ("invalid format char", c);
            }
        }
        if (er.MoveNext())
            throw new EvalException ("too many arguments");
        return;

        TOO_FEW_ARGS:
        throw new EvalException ("too few arguments");
    }
    private static void PrintInteger(int j, bool zero, int width,
                                     char format) {
        if (width == 0)
            Console.Write("{0:" + format + "}", j);
        else
            if (zero)
                Console.Write("{0:" + format + width + "}", j);
            else
                Console.Write("{0," + width + ":" + format + "}", j);
    }
} // Extra

Mono でのコンパイル例を示す。gmcs はジェネリック対応版 Mono C-Sharp コンパイラを意味する。 .NET Framework では,かわりに csc コマンドを使う。 /r: オプションで L2LispLib.dll を参照すること, /t: オプションで出力アセンブリの形式として library を指定することが要点である。

$ gmcs /d:TRACE /optimize /r:L2LispLib.dll /t:library PreludeExtra.cs
統合開発環境でも,具体的な操作方法はともかく,この二つの要点は同じです。


目次へ 7.へ


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