マクロ作成講座です。
F1ヘルプで長々と説明されているものを、もっと長々と説明してみようと思います。(え
他のコードの代わりとして使える識別子を、マクロ(macro)といいます。[脚注]
例えば、マクロ GREET が「mes "Hello, world!"」に展開されるものとして定義されているとき、
GREET
というスクリプトは、
mes "Hello, world!"
に置き換えられてから、処理されます。マクロをスクリプトに「置き換える」ことを「マクロを展開する」といいます。
いくつかの言語(HSP含む)では、実行する前に「プリプロセッサ」というソフト(または機能)が、ソースコードをいじくって、後で解析しやすい形に修整します。この時に、空白やコメントを削ったりするのですが、その際に、マクロを展開してくれます。
なぜこのような二度手間じみたことをするのかというと、「スクリプトを書きやすく、読みやすくするため」です。
例えば、意味のある数値には名前をつけた方が読みやすいですし、同じ文字列をいくつかの場所で使う場合は、何度も書くのは面倒なのでマクロが便利です。
また、switch 命令がマクロで実装されているといえば、その便利さがお分かりいただけるだろうか?
これはある意味「プログラミングのためのプログラミング」、すなわちメタ・プログラミング(meta programming)です。ちょっと言い過ぎですけれど。
HSPでマクロを作る(定義する)には、複数の方法があります。
第一に、これが一番汎用的なんですが、#define を使う方法です。
これについては、後ほど詳しく説明するので、今は飛ばします。
二つ目は、#const を使う方法です。
#const PI 3.14159 #const PI2 PI * 2 r = 1 mes PI2 * r // 半径 r の円の円周の長さ mes PI * (r * r) // 半径 r の円の面積※円周率 π の値は元々 M_PI として定義されています。
展開すると、次のようになります。
r = 1 mes 6.28318 * r mes 3.14159 * (r * r)
#defineと違って、#const の場合は、「静的な計算式」だけを定義できます。
展開されるときは、その式の値を計算した結果が展開されます。
つまり、式の計算をスクリプトの実行時ではなくコンパイル時に行うのがこの命令の目的です。
続いて、#enumです。これは、整数値限定ですが、とても便利な代物です。
なんと、定義するたびに数値が +1 されていくんです!!
……えっと、つまりですね (O-Oキラーン。
こんな感じ。
#enum IDW_MAIN = 0 #enum IDW_PROC // = 1 #enum IDW_STAGE // = 2 #enum IDW_BATTLE // = 3 #enum IDW_BATTLE_BIG // = 4 #enum IDW_SPACE_AREA // = 5 #enum IDW_CHAR_HERO // = 6 #enum IDW_CHAR_HEROINE // = 7
ウィンドウIDなど、「被ったら困るけど数値自身には興味ないです」って時に便利です。
今回はコメントで、各マクロに定義された値を書きましたが、通常は IDW_SPACE_AREA の値が何なのかは気にしません。重要なのは、それぞれが、他のどれとも同じ値ではないというコトです。
次のような命令があるとき、どのウィンドウのことを指しているのか分かりやすくなります。
gsel IDW_BATTLE
なお、定数値に展開されるマクロの命名規則は、#enum に書いたように、「すべて大文字」かつ「アンダーバー(_)区切り」というのが一般的です (たぶん)。
パッと見で読みにくいので、おすすめしませんが、この講座ではその規則で記述しました。
まず、簡単なものから始めましょう。これ以降は #define しか使いませんのであしからず。
とりあえずいくつかサンプルを:
// 汎例 #define マクロ名 置換後のスクリプト
#define 挨拶 mes "Hello, world!" #define 表示 mes #define 停止 stop 挨拶 表示 "こんにちは、デファインの世界へ!" 停止※HSPでは(空白以外の)全角文字も識別子として使用できます。
なんだか日本語プログラミング言語っぽくなりましたね! (なってないか)
↑のスクリプトを「展開」すると、こんな感じになります:
mes "Hello, world!" mes "こんにちは、デファインの世界へ!" stop
マクロ「挨拶」「表示」「停止」がそれぞれ単純に置換されているだけですが、
「変数に命令を代入」というようなことはHSPではできませんから、これはマクロならではの動作です。
勝手に命名してみました。
先ほどのようにただ単に置き換えているだけでは、あまり応用が効きません。
マクロを展開するに際し、どうしてもその“一部”を使用時に与えたくなります。
そこで、マクロに命令のように「引数」を与える方法があります。
#define 表示(%1) mes %1 #define 停止 stop #define 待機(%1) wait %1 #define 位置(%1, %2) pos %1, %2 #define 位置&表示(%1, %2, %3) pos %2, %3 : mes %1 位置 40, 60 表示 "よろしく!" 待機 50 位置&表示 "ここは(30, 80)と思われる。", 30, 80 停止
このように引数を持つマクロを「引数付きマクロ」と呼びます。
引数付きマクロを定義するには、マクロ名の後に括弧を付けて、「%N」(Nは1から始まる数字)という形式で仮引数を書きます。サンプルを見ての通り。
「表示」「待機」のようなマクロなら、先ほどのように単純置換マクロでも良いのですが、「位置&表示」マクロのように複雑な形で展開するには、引数が必要です。
引数が省略されたときの値を決めることもできます。
#define 表示(%1) mes %1 #define 停止 stop #define 待機(%1 = 0) wait %1 #define 位置(%1 = ginfo_cx, %2 = ginfo_cy) pos %1, %2 #define 位置&表示(%1 = "", %2 = ginfo_cx, %3 = ginfo_cy) pos %2, %3 : mes %1 位置 40, 60 表示 "よろしく!" 待機 50 位置&表示 "二行目の右の方に表示されるであろう。", 90 // カレント・ポジション Y 停止
仮引数を「%N = 省略値」という形式にしておくことで、その引数が省略された場合、%N はその省略値に展開されます。
一つ注意ですが、省略値にはいわゆる“アトム”——つまり、変数や命令などの名前、数値、文字列、だけを書けます。一方、「+」などの演算子を含む式や、( ) を伴う関数、( ) で括られた式、「カラのスクリプト」などは書けません。
しかし全く使えないわけではなく、その式自体を1つのマクロにして、そのマクロの名前を省略値にするという方法があります。
// 省略値が複雑な式である引数を持つマクロ #define current_position ("(" + ginfo_cx + ", " + ginfo_cy + ")") #define message(%1 = current_position) mes %1 pos 40, 40 message // カラのスクリプトに展開されるマクロ #define _empty mes _empty "hello!" // ← _empty の部分は空っぽになる
命令形式にできるんだから、関数形式、つまり「マクロ( 引数, ... )」にもしたいですね。出来ます。
キーワード「ctype」を使います。
#define 表示(%1) mes %1 #define 停止 stop #define 待機(%1 = 0) wait %1 #define 位置(%1 = ginfo_cx, %2 = ginfo_cy) pos %1, %2 #define 位置&表示(%1 = "", %2 = ginfo_cx, %3 = ginfo_cy) pos %2, %3 : mes %1 #define ctype 倍(%1 = 0) (2 * (%1)) #define ctype 自乗(%1 = 0) ((%1) * (%1)) #define ctype √(%1 = 0) (sqrt(%1)) #define ctype π 3.14159265358979323846264338 位置 40, 60 表示 "よろしく!" 待機 50 位置&表示 "二行目の右の方に表示されるであろう。", 90 // カレント・ポジション Y 表示 "4の倍は"+ 倍(4) +"である。" 表示 "4の自乗は"+ 自乗(4) +"である。" 表示 "4の√は"+ √(4) +"である。" 表示 "πの値は"+ π() +"である。" 停止※マクロ名は全角文字じゃないといけない、なんて縛りはないです。一応。
関数みたいですね。ただ単に ctype を追加するだけでした。
引数無しの場合は「π()」のようになります。ctype を指定すると、引数が無くても括弧は省略できないので注意してください。
※これのエラー表示はわかりやすいですが、一応、一度括弧を外して確認しておいてください。
もちろん、関数形式でも引数に省略値を付けることが可能です。サンプルの通り。
ただ、今回のように、意味なく省略値をつけるのはやめた方がいいです。引数を書き忘れても、動いてしまいます。
マクロの定義の中で、%1 などのパラメータに括弧 ( ) を付けているのは、計算の優先順位を守るためです。
例えば、次のスクリプトは何を表示するでしょう?
// 与えられた値を2回掛け合わせるマクロ……? #define ctype 自乗?(%1) %1 * %1 mes 自乗?(2 + 3) * 2
2 + 3 = 5 なので、5 * 5 を計算して 25、その2倍なので「50」が表示され……ません。
マクロを展開すると次のようになります。
mes 2 + 3 * 2 + 3 * 2 //= 自乗?(2 + 3) * 2
晴れて 14 が表示されます。もちろんコンピュータの計算は正しいです。
こういうミスは、マクロを使っているところでは発見できないので、とても分かりにくいバグ[脚注]となります。
括弧は大事!
もう一つ、引数付きマクロには問題があります。
マクロ引数はあくまで文字列として置換されるので、引数が複数箇所に展開されると、与えた式が複数回処理されることになります。
#define ctype Square(%1) ((%1) * (%1)) // 「自乗」マクロ a = "abcde12ab" // テキトー mes Square( int(strmid(a, 5, 2)) )
これを展開すると、次のようになります。
a = "abcde12ab" mes ( int(strmid(a, 5, 2)) * int(strmid(a, 5, 2)) )
展開すると一目瞭然ですが、int 関数と strmid 関数が2回ずつ呼ばれています。このような重い処理を2回も実行するのは無駄です。
また、無駄なだけならともかく、引数に“副作用を持つ関数”[脚注]が含まれていた場合、マクロの使用者から見て予想外の動作になってしまいます。
HSPの標準関数には、副作用持つ関数というのはありませんが、後の講座で紹介する #defcfunc や外部プラグインを使うと、副作用を持つ関数を作ることができます。
// 関数 inc(x) を、「変数 x の値を 1 大きくして、その大きくした後の値を返す」ものとして定義 #module #defcfunc inc var x x ++ return x #global // 「自乗」マクロ #define ctype Square(%1) ((%1) * (%1)) x = 2 if ( Square( inc(x) ) > 10 ) { mes "x^2 は 10 より大きい" }
この例では、x は inc によって 3 になり、その自乗は 9 なので条件を満たさない、と動くように見えます。
実際は以下のように inc が2回呼ばれるので、x は 4 になり、条件を満たします。
if ( ( (inc(x)) * (inc(x)) ) > 10 ) { mes "x^2 は 10 より大きい" }
この現象に起因するバグも、マクロを使用している側からは気付けないので、注意しておいてください。
僕は、マクロ引数は定義式の中で高々1回しか使わない、というコーディング規約(“自分ルール”)を使っています。
今回は正式名称です。
これを解説することが、このページの最大の目的です。がんばりましょう (主に僕が)。
特殊展開マクロとは、その名の通り、展開され方が特殊なマクロです。
とりあえずサンプルを見てください。
#define StartLoop %tLoop %i0 *%i #define BreakLoop %tLoop goto *%p1 #define EndLoop %tLoop goto *%o : *%o // サンプル・スクリプト randomize StartLoop redraw 2 color 255, 255, 255 : boxf : color pos rnd(600), rnd(400) : mes "サンプル・スクリプトです。" redraw 1 wait 20 EndLoop
サンプル・スクリプトの動作が謎ですが、StartLoop から EndLoop が無限に繰り返されているのがお分かりいただけたでしょうか。
これらのマクロの定義に含まれる、「%i」や「%o」などのことを、特殊展開マクロと呼びます。
スクリプトの最初の方に「#cmpopt ppout 1」という行を追加して、再び実行してみてください。すると、"hsptmp.i" というファイルが作成されます。
そのファイルには、プリプロセス後の(つまりマクロが全て展開された後の)スクリプトが保存されています。次のようになっているはずです。
randomize@hsp *_loop_0001 redraw@hsp 2 color@hsp 255, 255, 255 : boxf@hsp : color@hsp pos@hsp rnd@hsp(600), rnd@hsp(400) : mes@hsp "サンプル・スクリプトです。" redraw@hsp 1 wait@hsp 20 goto@hsp *_loop_0001: *_loop_0000※一部抜粋
これを整形して:
randomize *_loop_0001 redraw 2 color 255, 255, 255 : boxf : color pos rnd(600), rnd(400) : mes "サンプル・スクリプトです。" redraw 1 wait 20 goto *_loop_0001 *_loop_0000
※これらを実行しても、元のプログラムと同じ動作をします。
ラベルで作られた無限ループですね。
これは、スクリプトがプリプロセス処理され、「StartLoop」と「EndLoop」が展開された結果です。
まず、次の行を見てください。
#define StartLoop %tLoop %i0 *%i
これは、ループの始まりを定義しているマクロです。
「%tXXXX」は、マクロタグというものを定義しています。
このタグより右側にあるものは、このタグに含まれます。
%i などの「% と アルファベット1文字」は、前述の通り特殊展開マクロと呼び、
マクロタグ固有のスタックを弄ります。
マクロタグを設定しておくと、そのタグだけが使用することのできるスタックが作成されます。
それに、特殊展開マクロを使って、「スクリプト」を Push/Pop します。
記号 | 効果 |
---|---|
%tNAME | マクロタグを設定する。マクロの途中で上書き可能。 |
%n | ユニークな(= 一意な)識別子を生成し、配置する。 |
%i | ユニークな識別子をスタックにPushし、配置する。%i0 なら配置しない。 |
%o | スタックの一番上を Pop し配置する。%o0 なら配置しない。 |
%pN | スタックの上から N 個目を Peek して配置。N は省略すると 0 (一番上)。 |
%sN | マクロの引数 %N をスタックに Push する。 |
%c | 改行。スタックと無関係。コロン : でマルチステートメントするべき。 マクロを展開してプリプロセッサ命令を配置できるわけではないため、現状は使用する必要がない。 |
%% | 普通の % に展開される |
では、StartLoopマクロとEndLoopマクロの、スタックの動きを見ていきましょう。
StartLoop
EndLoop
さて、ボヤッとは分かってきたでしょうか。
特殊展開マクロの機能は、平たく言うと、「複数のマクロの間でデータを共有すること」と、「ユニークな識別子を生成すること」です。
このことを車輪の再発明とかなんとか (笑)。
※なお、実際の定義とは異なります。本物は common\hspdef.as を参照されたし。
まず、比較元の値をパラメータに、switch を定義します。
#define switch(%1) %tswitch %i = (%1)
ユニーク識別子を変数として使い、それに比較元の値を保存しています。
ちなみに、%s1 を使っても同じことができそうですが、これだとスタックに乗るのはあくまでスクリプトなので、
例えば switch ( rnd(3) ) などとなっている場合、
これを実行するたびに値が変わってしまって、意図した通りに動きません (この文章を公開した当初は、思いっきりこの間違いを犯していました)。
次に、case です。
#define switch(%1) %tswitch %i = (%1) #define case(%1) %tswitch if ( (%p) == (%1) ) {
与えられた値と、スタックの一番上(比較元の値)を等式で調べています。
このとき、比較元の値を左辺にして、型を比較元に合わせるようにしています。
……が、これでは { } の対応がおかしくなるのが目に見えていますね。
case の部分で、前の case の { を } で閉じる、という風に修整しましょう。
#define switch(%1) %tswitch %i = (%1) : if ( 0 ) { #define case(%1) %tswitch } if ( (%p) == (%1) ) { #define swend %tswitch } %o0
最初の case の } と最後の case の { に対応させるため、switch と swend に括弧を加えています。
なお、最初の if の条件式を真(true)にすると、後述の「fall through」という現象が起こってしまうため、意図した通りに動きません (この文章を公開した当初は、思いっきりこの間違いを(ry))。
では、ついでに default も定義しましょう。
#define switch(%1) %tswitch %i = (%1) : if ( 0 ) { #define case(%1) %tswitch } if ( (%p) == (%1) ) { #define default %tswitch } if ( 1 ) { #define swend %tswitch } %o0
条件を常に真 (非0) にして、確実に中の処理を実行します。
次に、swbreakです。
#define switch(%1) %tswitch %i0 %i = (%1) : if ( 0 ) { #define case(%1) %tswitch } if ( (%p) == (%1) ) { #define default %tswitch } if ( 1 ) { #define swend %tswitch } %o0 *%o #define swbreak %tswitch goto *%p1
スタックの、比較元の値の下に、ユニークな識別子を積んでおき、これを swend のところでラベルとして配置しています。
これにジャンプすれば、switch から脱出できますね。[脚注]
これで完成……ではありません。
これだと、fall through ができません!
さて、解決策は3通りあります。
fall through するための条件は「case の } の前まで実行されること」である、ということを考慮して、
#define switch(%1) %tswitch %i0 %i = (%1) : if ( 0 ) { #define case(%1) %tswitch goto *%i } if ( (%p1) == (%1) ) { *%o #define default %tswitch } if ( 1 ) { #define swend %tswitch } %o0 *%o #define swbreak %tswitch goto *%p1
case の最初の %i で識別子をPushし、最後の %o でPopしています。
このとき、その間にある条件式で、比較元の値が識別子の下(上から2番目)になることに気を付けてください。
まぁ、そのくらいです。
最後に、使うかどうかは分かりませんが、swredo を定義しておきましょう。これは、switch の処理を再び行う命令です。
#define switch(%1) %tswitch %i0 %i0 %i = (%1) : *%p2 : if ( 0 ) { #define case(%1) %tswitch goto *%i } if ( (%p1) == (%1) ) { *%o #define default %tswitch } if ( 1 ) { #define swend %tswitch } %o0 *%o %o0 #define swbreak %tswitch goto *%p1 #define swredo %tswitch goto *%p2
swbreak用のラベル名の下に、もう一つラベル名を積んでいます。ただの布石ですが……。
さ、これで完成です!
どうせなので、本家の switch の定義も載せておきます。
#define global switch(%1) %tswitch %i0 %s1 _switch_val=%p : if 0 { #define global case(%1) %tswitch _switch_sw++} if _switch_val == (%1) | _switch_sw { _switch_sw = 0 #define global default %tswitch } if 1 { #define global swbreak %tswitch goto *%p1 #define global swend %tswitch %o0 } *%o
※引用元「hspdef.as」(hsp3.2)。
ところで、本家の実装では、#define とマクロ名の間に global というのがありますね。
これは、「すべてのモジュールで永続的に使える」ということを宣言するキーワードです。
HSPにはモジュールという、プリプロセッサ命令や変数を部分的に独立する機能が付いています ( 詳しくはモジュール編で )。
通常は、モジュールの中と外(グローバル空間)でそれぞれ定義・宣言されたマクロや変数は
もう片方では使用できませんが、この global キーワードを使えば、すべて無視することが可能です。
#undef switch #undef case #undef default #undef swend #undef swbreak
HSPでは、同じ名前のマクロを複数定義することはできません。
「symbol is use [マクロ名] in line 行番号」というエラーが出ます。
マクロを削除するには、#undef が使用できます。あんまり馴染みのない命令ですが、再開発版 switch を使う場合は、定義の前に正規の switch をこれで un-define する必要があります。
「HSPのマクロ作成講座」というニッチなページを作ってみました。
アクセス増えるかな。
では、またいつか。
※追記(2014/02/21)
ちなみにタイトルの「“超”プログラミング」とは、普通は「メタプログラミング」(meta programming)といって、プログラム自体を扱うプログラムを作るです。
今回作ったマクロ(特殊展開マクロ)は、スクリプト自身に変更を加えるものなので、一種のメタプログラムだといえます。
※追記(2010/03/19)
※修正(2011/09/02) バグ修正、go_default 問題解決、swcontinue 追加
なんか、弄ってる内にこうなりましたので、一応書き足しておきます。
なお、この仕様では、case の式中で swthis が使用できないため、case_if を実装するにあたって、非常に格好悪い感じになってしまいました。実害はありませんけど……。
// 拡張版 switch 2011/09/02 // %p0: 比較元の値を持つ変数 // %p1: swend へのラベル // %p2: switch の先頭へのラベル (for: swcontinue) // %p3: switch の先頭へのラベル (for: swredo) #define global switch(%1) %tswitch %i0 %i0 %i0 %i0 swthis_bgn(%p) swdefault_init *%p2 : %p = (%1) : *%p3 : if ( 0 ) { #define global case(%1) %tswitch %i0 goto *%p } if ( (%p1) == (%1) ) { *%o #define global case_if(%1) %tswitch %i0 goto *%p } if ( (%1) ) { *%o #define global default %tswitch } swdefault_place_default : if ( 1 ) { #define global swend %tswitch } swdefault_place_swend : %o0 *%o %o0 %o0 : swthis_end swdefault_term #define global swbreak %tswitch goto *%p1 #define global swcontinue %tswitch goto *%p2 // 再分岐 (比較値を更新) #define global swredo %tswitch goto *%p3 // 再分岐 (比較値は同じ) #define global go_case(%1) %tswitch %p = (%1) : swredo // 比較値を変更して再分岐 #define global go_default %tswitch goto swdefault_label // default があればそこに、なければ swend に飛ぶ #define global xcase swbreak : case #define global xcase_if swbreak : case_if #define global xdefault swbreak : default // swthis を、switch とは別のスタックにも設定しておく // %p: 比較元の値を持つ変数 (switchから与えられる) #define global ctype swthis_bgn(%1) %tswitch_this %s1 #define global swthis %tswitch_this %p #define global swthis_end %tswitch_this %o0 // default 用のラベルスタック /* 生成する2つのユニーク識別子を A, B とする。 最初、A, B, A の順にスタックに積まれる ({ %p: A, %p1: B, %p2: A })。 @ %p2 の A は swdefault_label で参照するため。 default があるとき: 1. A が default に配置・除去される。B がスタック上に積まれる ({ %p: B, %p1: B, %p2: A })。 2. 一番上の B が swend に配置される。残り2つはそのまま取り除かれる。 default がないとき: 1. 一番上の A が swend に配置される。残り2つはそのまま取り除かれる。 したがって、A (swdefault_label) は default があるとき default を、ないときは swend (エラー部分) を指す。 */ #define global swdefault_init %tswitch_default %i0 %i0 swdefault_push(%p1) // [ A, B, A ] を積む #define global swdefault_term %tswitch_default %o0 %o0 %o0 #define global swdefault_place_default %tswitch_default *%o : swdefault_push(%p) // A を配置して除去, B を doubling-push #define global swdefault_place_swend %tswitch_default if ( 0 ) { *%p :\ logmes "go_default error: default doesn't exist.\n\t" + (__FILE__ + " (#" + __LINE__ + ")") : assert } #define global swdefault_label %tswitch_default *%p2 #define global ctype swdefault_push(%1) %tswitch_default %s1 // サンプルスクリプト #if 1 randomize r = rnd(6) switch ( r ) // case 0: // case 1: case_if ( swthis < 2 ): mes "[" + swthis + "] 2 未満は全部ここに集まれー!" swbreak case 2: mes "[" + swthis + "] default にも行ってあげよう。" go_default case 3: mes "[3] 幸運の数字です。やったね!" swbreak case 4: mes "[4] なんか嫌なので 3 に行きます。" go_case 3 // r = 3 : swcontinue default: mes "[*] default" swbreak swend stop #endif
go_default を書いたときに default がないと「ラベル無しエラー」が生じて困る、という問題があったので、それを解決しました。
かなり面倒なことをしているので、解説が超長いです。もっと楽にできそうな気がしますが……。