0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Magmaでメソッドチェーンを実現する

Last updated at Posted at 2023-05-03

本記事では,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のfunctionprocedureでは型の指定ができないため,普段行うような形のオーバーロードは(パッケージ外では)できない.

しかし,関数の定義時に中の変数の評価が行われることから,次のようにして非破壊的に演算子のオーバーロードが実現できる.

'+' := 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"
;

メソッドの引数を常にselfargsの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はプログラミング言語として見たときに色々とできないことが多いと思っていたが,思ったよりも可能性はあるのかもしれない.

まあ必要かといえばそんな気は一切しないのだが.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?