6. 組込関数の定義
- Web 閲覧用ソース (再掲)
- Copyright.txt
著作権表示: Lisp 関数
(copyright)で表示される。 - Help.txt
概要説明: Lisp 関数
(help)で表示される。 - Lib_AssemblyInfo.cs L2LispLib.dll のアセンブリ情報
- Lib_Funcs.cs L2LispLib.dll の構成要素: 組込関数の実装
- Lib_Interp.cs L2LispLib.dll の構成要素: インタープリタ本体
- Lib_Reader.cs
L2LispLib.dll の構成要素:
(read)の実装 - Lib_Types.cs L2LispLib.dll の構成要素: 様々な型の定義
- Main.cs L2Lisp.exe の構成要素
- Main_AssemblyInfo.cs L2Lisp.exe のアセンブリ情報
- Makefile
Mono 用: .NET Framework では
nmake CSC=cscとして使う。 - Prelude.l
初期化 Lisp スクリプト: Lisp 関数
(prelude)で表示される。 - PreludeExtra.cs 利用者定義の組込関数の例
- Copyright.txt
著作権表示: Lisp 関数
本章は 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)) =>x</summary>
[LispFunction("car")]
public static object Car(object list) {
return (list == null) ? null : ((Cell) list).car;
}
/// <summary>(cdr (cons x y)) => 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
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 式の引数として約束を与えたときの car,cdr と cons の違いを下記の例に示す。
car,cdr と異なり,cons が結果の値を構築するときには,その引数の具体的な値を得る必要はない。
遅延リスト構築の背景には cons のこの性質がある。
> (car ~(cons 1 2)) 1 > (cdr ~(cons 1 2)) 2 > (cons ~(cons 1 2) ~(cons 2 3)) (#<promise:25d17700> . #<promise:d5c91f40>) >
Lisp 関数の実装メソッドは必ずしも静的メソッドに限られない。
ファイル名の文字列を引数にとり,Lisp スクリプトとして実行する (load fileName)
の実装メソッドは次のように定義される。
/// <summary>(load fileName) => ファイルを Lisp プログラムとして評価
/// </summary>
[LispFunction("load")]
public object Load(object fileName) {
return Run(Lines.FromFile((string) fileName));
}
Interp クラスの非静的メソッド Run (5.1 節)
を呼び出すためには,このメソッド自身が非静的でなければならない。
Lisp 関数 eval,apply などの実装メソッドについても同様である。
そして,このことが,ほかならない Interp クラスに Lisp 関数の実装メソッドを定義する理由である。
もしもほかのクラスで Lisp 関数の実装メソッドを定義し,そこから Interp
クラスのメソッドを呼び出すならば,典型的には,Lisp
プログラムでその関数の引数として陽に (cs-self) の値を与える
(あるいはラムダ式でそのようなクロージャを構成する) 必要がある。
/// <summary>(cs-self) => インタープリタ自身のオブジェクト
/// </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 組込関数のロードを行う Interp の LoadNatives
メソッドを,二つの公開メソッドとして用意する。
一つは型オブジェクトを引数にとり,もう一つは (なんらかの型のインスタンスである) 任意のオブジェクトを引数にとる。
/// <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 属性が指定されていれば,非
null の LispFunctionAttribute インスタンスになる。
LispFunctionAttribute インスタンスとして
attr から Lisp 関数としての名称 name
と,約束を評価しないかどうかのフラグ is_lazy を得る。
method.GetParameters() からメソッドの仮引数の情報を得る。
ただし,仮引数の情報のうち実際に使うのは引数の個数 arity だけである。
arity に該当する (2.3 節 で述べた)
定義済みのデリゲートの型オブジェクト dt を選出する。
dt と method,そして引数 target
から Delegate.CreateDelegate によってデリゲート・インスタンス d を得る。
このとき,method が静的メソッドを表すかどうかで
target を使わないか使うかを場合分けする。
name に該当するシンボルをキーとして,インタープリタのシンボル・テーブル
symbol に d を格納する。
これは 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.dll の Extra クラスから 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 {
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 で印字する。
可能な変換指定子は d,x,X,s,% と
p,r である。
整数に対する d,x,X は 0 フラグと最小フィールド幅を指定できる。
Ruby/Python と同じく s に対する実引数は文字列に限られない。
s は実引数に対し,標準の文字列化メソッドである ToString() を呼び出す。
p と r はそれぞれ 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