本記事では,Magmaで無理やりメソッドチェーン的なものを実装するためのちょっとしたテクニックを紹介する.
変数の動的取得
関数の中で外の変数を取得することはできる.
test := 1;
function get()
return test;
end function;
get(); // 1
しかし,関数の中の変数test
は,関数get
を定義した時点で値が定まってしまう.
test := 1;
function get()
return test;
end function;
get(); // 1
test := 2;
get(); // 1
関数を実行したときの値を見てほしいのならば,eval
文を使えばよい.
test := 1;
function get()
return eval "test";
end function;
get(); // 1
test := 2;
get(); // 2
言うまでもないが,色々な言語で言われている通り,eval
文は意図しない命令が実行されるというセキュリティ上のリスクを抱えている.(変数を書き換えることはできないようだが)
注意して扱わないといけない.
任意文字列を名前に持つ変数の定義・参照
本来,変数名に使える文字には制限がある.自分が知っている限りだと次の通り:
- 使用可能な文字は英数字(大文字・小文字),アンダースコア(
_
)のみ. - 変数名の頭に数字があってはならない.
正規表現で表すならば,/^[A-Za-z_][A-Za-z_0-9]*$/
といったところか.
しかし,このようなルールはシングルクォーテーションで囲ってしまえばすべて無視できる.
' ' := 4; print ' ';
'!"#$%&' := func<x | x + 1>; print '!"#$%&';
'1' := 0; print '1';
シングルクォーテーションはエスケープ(\'
)しなければならないが,(ASCII印刷可能文字における)すべての文字を使用することができる.
実際,演算子などはこのシングルクォーテーションを用いて定義されている.
intrinsic '+'(x::TypeName, y::TypeName) -> TypeName
...
end intrinsic;
この機能を用いれば,誤って変数を上書きされる心配が減りそうだ.
あるいはこの記述が「書き換えるな」という強い意思表示になるのかもしれない.
Intrinsicや演算子のオーバーロード
パッケージ外の次のような演算子オーバーロード(のつもり)の定義は,極めて破壊的な行為である.
'+' := function(foo, bar)
return SpecialFunction(foo, bar);
end function;
Magmaのfunction
やprocedure
では型の指定ができないため,普段行うような形のオーバーロードは(パッケージ外では)できない.
しかし,関数の定義時に中の変数の評価が行われることから,次のようにして非破壊的に演算子のオーバーロードが実現できる.
'+' := function(foo, bar)
try
return foo + bar; // ここの '+' は上書きする前のintrinsicである
catch e
if "Bad argument types" in e`Object then
return SpecialFunction(foo, bar);
else
error Error(e);
end if;
end try;
end function;
例えば,+
演算子を文字列の結合cat
代わりに使うなんてこともできる.
'+' := function(foo, bar)
try
return foo + bar;
catch e
if "Bad argument types" in e`Object then
return foo cat bar;
else
error Error(e);
end if;
end try;
end function;
"foo" + "bar"; // foobar
他にも,配列の添字を0始まりにすることができる.
function zero_start()
'[]' := func<a, i | a[i+1]>; // この関数内でのみ有効
return [3, 4, 5][1];
end function;
zero_start(); // 4
[3, 4, 5][1]; // 3
もちろん,何度も上書きをして問題ない.(パフォーマンスは度外視である)
'+' := function(foo, bar)
try
return foo + bar;
catch e
if "Bad argument types" in e`Object then
return foo cat bar;
else
error Error(e);
end if;
end try;
end function;
'+' := function(foo, bar)
try
return foo + bar;
catch e
if "Bad argument types" in e`Object then
return bar cat Sprintf("%o", foo);
else
error Error(e);
end if;
end try;
end function;
print 2 + "O"; // O2
print "Hello " + "World"; // Helo World
もちろん,新たに定義する型の組み合わせを指定する書き方もある.
'+' := function(foo, bar)
try
return foo + bar;
catch e
if Type(foo) eq MonStgElt and Type(bar) eq MonStgElt then
return foo cat bar;
else
error Error(e);
end if;
end try;
end function;
'+' := function(foo, bar)
if Type(foo) eq MonStgElt or Type(bar) eq MonStgElt then
return Sprintf("%o%o", foo, bar);
else
return foo + bar;
end if;
end function;
パッケージを使えばいいって?それだとMagma Calculatorが使えないではないか.
メソッドチェーンもどきの実装
本記事の主題に入る.
色々なデータ構造上の制約があるものの,構造体を活用することでそれっぽい機能を実現することができる.
'::METHOD' := recformat<object, key>;
CLASS_NAMES := [Strings() | ];
'::Is_Class' := function(record)
// 本来の '.' 演算子が Recに対応していないことから,
// 次のような処理にしちゃっても多分問題ない.
// return Type(record) eq Rec;
if Type(record) ne Rec then
return false;
end if;
names := eval "CLASS_NAMES";
assert ExtendedType(names) eq SeqEnum[MonStgElt];
return exists{name : name in names | (eval name) cmpeq Format(record)};
// name に相当する変数が実行時にdeleteされているとエラーになる.
// かといって eval "assigned " cat name はうまく動かない.
end function;
'::Is_Method' := function(record)
if Type(record) ne Rec then
return false;
end if;
return Format(record) cmpeq '::METHOD';
end function;
'.' := function(ope1, ope2)
if '::Is_Method'(ope1) then
assert Type(ope2) eq Tup;
obj := ope1`object;
key := ope1`key;
return (obj `` key)(obj, ope2);
elif '::Is_Class'(ope1) then
assert Type(ope2) eq MonStgElt;
if Type(ope1 `` ope2) eq UserProgram then
return rec<'::METHOD' | object := ope1, key := ope2>;
else
return (ope1 `` ope2);
end if;
else
return ope1.ope2;
end if;
end function;
// 使用例
CLASS_TEST := recformat< v: RngIntElt, step: UserProgram, add: UserProgram >;
Append(~CLASS_NAMES, "CLASS_TEST");
obj := rec<CLASS_TEST |
v := 0,
step := (function(self, args)
self`v +:= 1;
return self;
end function),
add := (function(self, args)
assert #args ge 1 and Type(args[1]) eq RngIntElt;
// 引数の評価には car< ... > が便利:
// args := car<Integers()> ! args;
// あるいは, f, args := IsCoercible(car<Integers()>, args); assert f;
self`v +:= args[1];
return self;
end function)
>
;
obj
."step".<>
."add".<2>
."v"
;
メソッドの引数を常にself
とargs
の2つにしなければならなかったり,メソッドチェーンを行うためには第1返り値をself
(あるいはそれと同じ種類の構造体)にしなければならなかったりなど色々な面倒があるが,とにかくメソッドチェーンっぽい記述ができた.継承などができないのでクラスというにはあまりにもおこがましいが,突き詰めればもっと色々なことができるかもしれない.
なお,プロパティ名をシングルクォーテーションで囲うこともできる.こちらの方はobj `` "*v"
で簡単にアクセスできてしまうが,プライベートプロパティを示す良い目印にはなるだろう.
// (それまでの処理は省略)
// 使用例
CLASS_TEST := recformat< '*v': RngIntElt, step: UserProgram, get_v: UserProgram >;
Append(~CLASS_NAMES, "CLASS_TEST");
obj := rec<CLASS_TEST |
'*v' := 0,
step := (function(self, args)
self`'*v' +:= 1;
return self;
end function),
get_v := (function(self, args)
return self`'*v';
end function)
>
;
obj
."step".<>
."step".<>
."step".<>
."step".<>
."get_v".<>
;
Magmaはプログラミング言語として見たときに色々とできないことが多いと思っていたが,思ったよりも可能性はあるのかもしれない.
まあ必要かといえばそんな気は一切しないのだが.