====== リロード ======
書きかけ。コードチェックしてません。
===== リロードとは =====
アプリケーションを動作させたまま、書き換えたスクリプトを反映させる作業です。
これを行うことで、アプリケーションの再コンパイルどころか再起動さえほとんどしないでトライ&エラーを行うことができます。
===== 最初の一歩 =====
プログラムが、filelocalに存在しているXtalオブジェクトだけを利用し、またC++側に保持しない場合、そのファイルをcompile_file/loadしなおすだけで大丈夫です。
// test.xtal(リロード前)
fun f(s1, s2){
return s1;
}
// test.xtal(リロード後)
fun f (s1, s2){
return s2;
}
// c++ file
using namespace xtal;
AnyPtr filelocal(load("test.xtal"));
StringPtr str = filelocal->member(Xid(f))->call("foo", "bar");
str->p(); // > foo
stdout_stream()->println("ファイルの変更を待ちます。変更したら何か入力してください。");
// ここでtest.xtalを書き換えます
std::string buf;
std::cin >> buf;
stdout_stream()->println("リロードします。");
filelocal = null;
full_gc();
filelocal = load("test.xtal");
StringPtr str = filelocal->member(Xid(f))->call("foo", "bar");
str->p(); // > bar
なお、次のような呼び出すためにカウンタを1ずつ大きくして返すような関数を含む場合、リロードすると、つまりコンパイル・実行しなおすわけですから、カウンタが0に初期化されます。
counter : 0;
fun CountUp(){
++counter;
return counter;
}
このように、関数外環境に依存する関数の他にも、1回だけ呼ばなくてはならない初期化関数などは、リロード時に呼ばれないようにする、もしくはリロードしないファイルを決めるなどといった工夫が必要となります。
===== クラスのリロード =====
クラスのリロードも、上でやったようにfilelocalのクラスオブジェクトを差し替えればそれ以降のインスタンス生成は新しいクラスが使われます。しかし、これでは元のクラスのインスタンスは変更されず、新しいクラスへ全部コピーする(そしてそのために全部覚えておく)などという処理が必要になり、「そのまま動作を継続させる」ことを妨げる要因となります。
これを解消するためXtalには**元のクラスのメンバテーブルを、引数のクラスのメンバテーブルで上書きする**ことのできるClass::overwriteというメソッドが用意されています。
Src class を Dst class で上書きしたとすると、
* 今後Srcインスタンスが呼び出すメソッドもDstで定義されたものになる
* 新しく追加された(DstにあってSrcにない)インスタンス変数はundefinedになる
* 削除された(DstになくてSrcにある)メンバ変数はDstからはアクセス不能(そもそもコンパイルエラー)
* 削除されたメソッドは、Dst内部から呼び出せないが、dst.methとは呼び出せる
* メンバの処理が終わった後、各インスタンスに付き1回だけDstのreloadedメンバが呼び出される(存在するとき)
次のコードは、size, posメソッドがそれぞれ多値で返していたRectを、Pointクラスで返すようにしたNewRectでoverwriteした例です。
// .xtal
class Point{
+ _x : 0.0;
+ _y : 0.0;
initialize(_x : 0.0, _y : 0.0){
}
to_s(){
return %f[Point(%f, %f)](_x, _y);
}
}
class Rect{
_x : 0.0;
_y : 0.0;
_w : 0.0;
_h : 0.0;
_removed : "_removed";
initialize(_x : 0.0, _y : 0.0, _w : 0.0, _h : 0.0){
}
pos(){
return _x, _y;
}
size(){
return _w, _h;
}
remove_method(){
return _removed;
}
to_s(){
return %f[Rect(%f, %f, %f, %f)](_x, _y, _w, _h);
}
reloaded(){
"Rect reloaded called".p;
}
}
rect : Rect(w : 32.0, h : 64.0);
rect.p;
rect.class.p;
rect.pos().p;
rect.size().p;
(rect.?remove_method).p;
"===== Reload =====".p;
// realod
class NewRect{
_x : 0.0;
_y : 0.0;
_w : 0.0;
_h : 0.0;
_pos : null;
_size : null;
_added : null;
initialize(_x : 0.0, _y : 0.0, _w : 0.0, _h : 0.0){
_pos = Point(_x, _y);
_size = Point(_w, _h);
}
pos(){
return _pos;
}
size(){
return _size;
}
to_s(){
return %f[Rect(%, %, %, %, %, %, %)](_x, _y, _w, _h, _pos, _size, _added);
}
reloaded(){
"NewRect reloaded called".p;
_pos = Point(_x, _y);
_size = Point(_w, _h);
}
}
Rect.overwrite(NewRect);
rect.p;
rect.class.p;
rect.pos().p;
rect.size().p;
(rect.?remove_method).p;
newrect : NewRect();
(rect.class === newrect.class).p;
新しく追加したインスタンス変数は_pos, _size, _addedで、そのうち_addedにのみreloadedで値を設定していません。_addedのデフォルト値nullも設定されずにundefinedになっています。
なお、".?"というのは、「存在していればメソッドを呼び出す、存在していなければundefinedを返す」演算子です。
また、
return %f[Point(%f, %f)](_x, _y);
のように"%f[...]()"となっているのは、フォーマット生成式で、文字列をprintfのような形式で生成できる式です。
===== globalとリロードの深い関係 =====
Xtalは、クラスへのメンバ(メソッド・クラス定数)の追加を行うことができますが、再定義・代入は許可されていません。
class Foo{
}
Foo::foo : method(){
"method foo".p;
}
Foo().foo();
// メンバの再定義をしてみようとすると実行時にエラー
try{
Foo::foo : method(){
"method new foo".p;
}
} catch(e){
e.p;
}
// メンバへの代入をしてみようとするとコンパイルエラー
/*
Foo::foo = method(){
"method new foo".p;
}
*/
これはClassを継承しているLibに関しても同じです。そのため、Libなどに定義した関数などをリロードするためにはsingletonに入れてそれごとoverwriteする、などという処理をする必要があります。
これが面倒であるため追加されたのがglobalというグローバル定義領域です。これは、Xtalスクリプトからの再定義が許可されたClassです。そのため、リロードする際には再定義すればよいのです。これにより、スクリプトを(以前実行されたかどうかをチェックする必要なく)実行するだけですみます。
// 変更前
fun global::foo(){
"変更前".p;
}
// 変更後
fun global::foo(){
"変更後".p;
}
// main.xtal
load("foo.xtal");
global::foo();
// ここで書き換えを待つとする
load("foo.xtal");
global::foo();
これと同じことをlib::fooでやろうとすると、再定義エラーが出るはずです。
また、globalの特徴として、「クラスのリロードは勝手にoverwriteメソッドを呼び出す」というものも挙げられます。同じ名前で再定義すると、自動でoverwriteメソッドを呼び出してくれる便利機能です。
// 変更前
class global::Foo{
to_s(){
return "Before Foo";
}
}
// 変更後
class global::Foo{
to_s(){
return "After Foo";
}
reloaded(){
"After Foo reloaded".p;
}
}
// main.xtal
load("foo.xtal");
foo : global::Foo();
foo.p;
// ここで書き換えを待つとする
load("foo.xtal");
foo.p;
class global::Foo{}
というのは、
global::Foo : class{}
と同じ意味ですから、リロードしたときにはglobal::Fooが再定義されます。
globalオブジェクトは、再定義時にそのオブジェクトがクラスかどうかをチェックし、そうであったらoverwriteメソッドを呼び出します。そのため、変更後クラスのreloadedが呼ばれ、またそれまでのインスタンスのメソッドテーブルが差し替えられるのです。
===== リロード時の注意 =====
globalに定義されている変数を他の変数に代入した後にリロードすると、globalの変数は変わりますが、代入された変数の値は変わりませんから、再度代入する必要があります。
これが問題になるのは、高速化のためにローカル変数・メンバ変数に代入したとき、コールバック関数として登録・保持したとき、が主に想定されます。
前者は次のようなコード
// foo.xtal
// こちらだけリロードする
// 変更前
global::foo : 100;
// foo.xtal
// こちらだけリロードする
// 変更後
global::foo : 200;
// main.xtal
load("foo.xtal");
foo : global::foo;
reload();
foo.p;
後者は次のようなコード
// foo.xtal
// 変更前
fun global::callback(){
"callback".p;
}
// foo.xtal
// 変更後
fun global::callback(){
"callback reloaded".p;
}
// main.xtal
load("foo.xtal");
class CallBacker{
_funs : [];
initialize(){}
add(f){
_funs.push_back(f);
}
call(){
_funs{
it();
}
}
}
callbacker : CallBacker();
callbacker.add(global::callback);
callbacker.call();
reload();
callbacker.call();
callbacker.add(global::callback);
callbacker.call();
どちらも、本質的なところは、最初に触れたように「globalのオブジェクトを他の変数に代入しているから」です。
以下、図で説明します。
まず、globalにcallback関数を定義します。すると、globalという領域に、"callback"という名前とfun(){"callback".p;}という内容、というペアが追加されます。global::callbackと書くと、「globalという領域から"callback"という名前を探し、それの内容を返す」という意味になります。
fun global::callback(){
"callback".p;
}
"global::callback"という名前の関数を実行する、というイメージではなく、"global::callback"によって得た関数オブジェクトを実行する、というイメージを持ってください。
次のコードによって、"filelocal::f"を"global::callback"で得た関数オブジェクトで定義しています。
f : global::callback;
この時点の概念図は次のようになります。
ここで、次のようなcallback関数でリロードします。
fun global::callback(){
"callback reloaded".p;
}
すると、次のコードを実行したことに等しくなります。
global::callback = fun(){
"callback reloaded".p;
}
この時点で、fとglobal::callbackの指す内容が異なってしまいます。
これは特別な考えではなく、次のようなコードと同じような結果です。このコードを不思議に思う人はいないでしょう。
class Foo{
+ _hoge : null;
}
// fun global::callback(){...} に対応
foo : Foo();
foo.hoge = 100;
// f : global::callback; に対応
bar : foo;
// fun global::callback(){...} (reload) に対応
foo = Foo();
foo.hoge = 200;
foo.hoge.p;
bar.hoge.p;
foo.hogeとbar.hogeは異なる値になっています。つまり、fooとbarは異なるオブジェクトになっています。
少しだけわかりにくいのがクラスをリロードしたときのメソッドの扱いです。
class CallBacker{
_o : null;
_m : null;
initialize(_o, _m){}
call(){
_o.callmethod(_m);
}
}
class Foo{
p(){
"Foo".p;
}
callmethod(m){
m();
}
}
class NewFoo{
p(){
"New Foo".p;
}
callmethod(m){
m();
}
}
foo : Foo();
callbacker : CallBacker(foo, Foo::p);
callbacker.call();
Foo.overwrite(NewFoo);
callbacker.call();
Class:overwriteがメソッドに対して何をやっているかというと、Foo::method1 = method(){}; Foo::method2 = method(){}; ...... とやっているに過ぎません。
CallBackerにFoo::pを渡すとき、「Fooのpというメソッドという情報」ではなく、「その時点でFooでpという名前で定義されていたmethodオブジェクト」を渡しているのです。
再度まとめますが、リロードするオブジェクトをどこかに代入していたとき、その変更を反映させたければ再代入する必要がある、となるわけです。
==== 対応策 ====
といっても、再代入が非常に難しい場面もあると思います。
速度が気にならない場所に関しては、関数を1つかませることによってリロードしたときに再代入せずとも変更を反映させることができます。
// foo.xtal
// 変更前
fun global::callback(){
"callback".p;
}
// foo.xtal
// 変更後
fun global::callback(){
"callback reloaded".p;
}
// main.xtal
load("foo.xtal");
class CallBacker{
_funs : [];
initialize(){}
add(f){
_funs.push_back(f);
}
call(){
_funs{
it();
}
}
}
callbacker : CallBacker();
callbacker.add(fun(){global::callback()});
callbacker.call();
reload();
callbacker.call();
callbacker.add(fun(){global::callback();});
callbacker.call();
このようにすると、「CallBacker::callを呼び出した時点のglobal::callback()を呼び出す関数オブジェクト」を登録することになり、「登録した時点のglobal::callbackの関数オブジェクト」ではなくなるため、うまく変更が反映されることになります。