というか変数の宣言がないです。
まあ、マクロ変数での%local %globalがあって、難しいちゃ難しい世界ですけど、そんなに複雑なマクロ考えない場合はあんまり関係ないし、よくわからないまま問題なくプログラム書けてる場合も多いはずです。
ところがDS2でプログラム書く時には、スコープを強く意識しないといけません。
次のコードは、1変数1obsで変数xに0の値が入ったデータセットA1を作成するためのコードですが
実行すればエラーになります。
proc ds2 libs=work;
data A1(overwrite=yes);
method run();
dcl double x;
x = 0;
output;
end;
enddata;
run;
quit;
なぜなのか?
dcl(declare)ステートメントでxを宣言してますが、それがrun()メソッドの中に書かれています。
メソッド内でのdcl、或いはパラメータとして宣言された変数はローカル変数となります。
ローカル変数はスコープがメソッド内に限定されるため、その外側には存在し得ないので
データセットに残ることもできません。なので、何も変数なしでデータセット作ろうとしたのでエラー
ということです。
なので、以下のように宣言の位置をメソッドの外にだしてやります。
proc ds2 libs=work;
data A1(overwrite=yes);
dcl double x;
method run();
x = 0;
output;
end;
enddata;
run;
quit;
このあたりは、やっぱり忘備録の記事が秀逸なので、不安のある方はそちらで復習してください。
・DS2プロシジャ入門2:変数の宣言
http://sas-boubi.blogspot.jp/2015/06/ds22.html
で、やっとここから本題への繋ぎなんですが、ひとつのステップの中で、グローバル変数とローカル変数に同じ名前をつけたいケースがでてきます。
例えば、何らかのメソッドで値をパラメーターで受け取ったりして、加工して、他のメソッドに渡したい場合などにメソッドの外に、消さずに出したいわけですので、グローバル変数に格納するわけです。
メソッドを経由する都度、別の名前になるようにしていたら、わけがわからなくなります。
意味として同一のものは同じ名前にしておかないといけません。
ところが実際コーディングしてみると、変数名は同じなので、それがグローバル変数をさしているのか、ローカルをさしているのかがわからなくなります。
そこで、グローバル変数を明示的に指定する場合に変数名の前にthis.というものをつけて区別しようというわけです。
以下のコードはSASのリファレンスから抜き出して一部加工したサンプルです
proc ds2 libs=work;
data A2(overwrite=yes);
declare double x; /* declare global x */
method run();
declare double x; /* declare local x */
this.x = 1; /* assign 1.0 to global x */
x = 0; /* assign 0.0 to local x */
output; /* global x with 1.0 output */
end;
enddata;
run;
quit;
グローバル、ローカル両方でxを定義し、両方に値を代入していますが、this.をつけたxだけが
グローバル変数と解釈され、処理されるので結果にも1の方が残ります。
さて、ここからが本題です。
例えば以下のデータセットがあって
data Q1;
x=1;output;
x=3;output;
x=5;output;
x=8;output;
x=2;output;
x=8;output;
x=1;output;
x=4;output;
run;
順次オブザベーションを読み込んでいき、1obsごとにそれまでのxの値の累積合計を
ログにputする。ただしx=8となったときは累積合計を0にリセットするという仕様のプログラムを
作りたいと思います。しかもその仕組みは流し込むデータセットを変えたり、他の処理も後で
追加したいため、パッケージ化したいという要望があるとします。
さて、どんな風にメソッドを作りパッケージを組み、どうやって呼び出してやりましょうか?
書き方はいくらでもあるでしょうが、基本的な考え方として、機能ごとにメソッドを分けてやれば
大体正解の場合が多いです。
そこで、累積合計の計算で1メソッド、ログへのputで1メソッドの構成を考えます。
ついでに、実は今回は無くてもいいのですが、コンストラクタの実行について追加で説明しておきたいことが
あるのでコンストラクタに処理が開始される旨のメッセージと、累積合計の初期化(=0)を入れておきましょう。
別に無くてもいいといったのは、今回合計の計算にsum関数使うので、初期時点の累積合計値が欠損でも大丈夫なのでということです
はい、じゃあできました、こんな感じかな
proc ds2 libs=work;
package pkg_mes /overwrite=yes;
dcl varchar(100) message;
dcl double total;
method pkg_mes();
total=0;
setmes('コンストラクタでの生成メッセージ:total=',0);
putmes();
end;
method setmes(varchar(90) message , double num );
total = sum(total,num);
if num= 8 then total=0;
this.message = message || total;
end;
method putmes() ;
put message;
end;
endpackage;
run;
quit;
はい、エラー!コンパイルできずにパッケージは作られませんでした。
なんでか?
それはコンストラクタpkg_mesメソッドの中でsetmes(計算部分)とputmes(出力部分)を呼び出しているのですが、この時点でsetmesとputmesはまだ定義コードが読み込まれていないので、SASはメソッドだと思えずに、括弧があるから関数だと思って呼び出そうとするけど、そんな関数ないからエラーになります。その下にすぐメソッド定義書いてるから、ちょっと待ってといっても聞いてくれません。
なので、コードの順番を入れ替えてpkg_mesメソッドを他2つのメソッドの下に記述すればいいんですけど、それは気持ち悪い!なんで、一番最初に実行される部分を、最後にかかなきゃならんのか。
だいたいメソッドは入れ子になることが多いのに、その度に並び替えゲームするわけにいかないですよ。
SASマクロは、どれだけ入れ子にしても展開される時点で存在してればよく、コーディングの順番を意識する必要がなかっただけに違和感がありますね。
で、解決法はいたって簡単、後で記述されているメソッドの中身を先行するメソッド内で記述する場合、グローバルの領域に「forward メソッド名」と記述して、あらかじめSASに、forward指定しているのは後で定義してるメソッドだから慌てんなといってあげればよい。
ということで、書きなおし
proc ds2 libs=work;
package pkg_mes /overwrite=yes;
dcl varchar(100) message;
dcl double total;
forward setmes;
forward putmes;
method pkg_mes();
total=0;
setmes('コンストラクタでの生成メッセージ:total=',0);
putmes();
end;
method setmes(varchar(90) message , double num );
total = sum(total,num);
if num= 8 then total=0;
this.message = message || total;
end;
method putmes() ;
put message;
end;
endpackage;
run;
quit;
でOK
やっと呼び出しまで来た。
今回はinitメソッドはいらないんだけど、initメソッドとコンストラクタの実行だと
コンストラクタの方が早い(dclの時点だから)を再確認して欲しいので、あえていれてます
proc ds2 libs=work;
data _null_;
dcl package pkg_mes p();
method init();
p.setmes('init内での生成メッセージ:',0);
p.putmes();
end;
method run();
set Q1;
p.setmes('run内での生成メッセージ:',x);
p.putmes();
end;
enddata;
run;
quit;
結果、
というわけです。
今回はただ、putするだけで、データセットを作成しないのでメソッドにreturnをつけないサンプルでしたが今後はもう少し実務的なサンプルも上げれればいいかなと思ってます。
あ~、長い記事だった
0 件のコメント:
コメントを投稿