MikoScript 言語仕様
構造体
「構造体」とは、あるまとまったデータ構造(或いはデータ構成)を表わすものです。
本言語では、構造体は、「複合箱」になるので、その複合箱の構造が、その構造体の構造
に対応します。そのため、構造体の設定は、構造体の雛型を定義するのではなく、構造体
を構成する実体、つまり、構造体の複合箱の中身を、実行文によって、構成していくこと
になります。かくて、構造体の構造は、プログラムの実行時に動的に自由に変化できます。
ちなみに、コンパイラ言語等では、通常、構造体の構造は、プログラムのコンパイル時に
確定されてしまい、実行時には変化できません。
●構造体の設定
構造体の複合箱の中にある箱を、その構造体の「メンバー」と言います。つまり、構造
体は、メンバーの集まりで構成されます。構造体の設定では、そのメンバーを決めること
が主体になります。
構造体は、通常、「構造体設定文」の実行によって、設定します。この構文の書式は、
以下のようになります。
構造体名 ::=
{
メンバー設定部
}
ここで、「構造体名」には、設定対象の構造体の名前を規定します。この名前は、その
構造体の複合箱を代表する最外の箱の名前です。これは、単一の識別名だけでなく、一般
に「箱の経路」を規定できます。この箱の経路の基点は、スコープ規定演算子によって、
規定できます。それは通常の箱名の場合と同じです。構造体名に連想名を含む場合、その
インデックスには変数が使えます。
::= という記号が、「構造体名」の直後にあります。これは、一見、演算子のようです
が、あくまで構文構成上の記号で、文法的には、演算子ではありません。本構文は、この
ような記号を使わないで、struct という予約語を使って、C 言語風の構文にすることも
可能でしたが、諸般の混同を避けるために、敢えてこのような書式にしています。
「メンバー設定部」は、::= 記号に続く波括弧 { } で囲われたブロック内にあります。
ここには、その構造体を構成するメンバーを設定するための一連の実行文を記述します。
「メンバー設定部」のブロック内では、メンバースコープが、「構造体名」で規定された
箱内直結のスコープに変わります。そのため、箱名の先頭に「.」のスコープ規定演算子
を付ければ、そのクラスのメンバーになります。箱名の先頭にスコープ規定演算子が何も
付いていなければ、それは、ブロックの内外で共通のデフォールトスコープになるので、
注意が必要です。なお、ブロックの内側と外側で変わるのは、メンバースコープだけで、
それ以外は何も変わりません。
「構造体設定文」は、その末尾(波括弧のブロックの後)に、セミコロンは不要です。
但し、付けても、文法エラーにはなりません。
以下に、構造体を設定する例を示します。
S ::=
{
.A = 1;
.B = 2;
}
この実行によって、デフォールトスコープ(この場合、現実行関数のローカルスコープ)
内に、S という名前の構造体が生成され、そのメンバーに、箱 A と箱 B が登録されます。
これらのメンバーの初期値は、それぞれ、整数値 1, 2 に設定されています。このように、
構造体のメンバーは、代入によって生成します。その際、初期値が設定できます。なお、
代入以外でも、構造体のメンバーが生成できますが、これについては、次節で説明します。
上例の構造体 S と同じ構造の複合箱は、「構造体設定文」を使わずに、以下のように、
生成することもできます。
S.A = 1;
S.B = 2;
しかしながら、「構造体設定文」を使わずに生成した複合箱には、構造体としての属性が
設定されません。そのため、次節で説明する各種の入出力に使えません。また、構造体は、
次章で説明する「クラス」の機能を担えますが、構造体の属性が設定されていない複合箱
には、その機能はありません。複合箱を構造体として構築するには通常、「構造体設定文」
を実行する必要があります。
「構造体設定文」のメンバーは、既に設定されている構造体を使って、設定することが
できます。例えば、上例の構造体 S を使って、新規の構造体 T を設定するには、
T ::=
{
.X = S;
.Y = 0;
}
とします。この実行で、デフォールトスコープ内に、T という名前の構造体が生成され、
そのメンバーとして、箱 X と箱 Y が登録されます。箱 X には、構造体 S のコピーが、
代入されているので、T.X.A には、整数値 1 が入っていて、T.X.B には、整数値 2 が
入っています。
「構造体設定文」は、入れ子にできます。つまり、「構造体設定文」の中で、さらに、
「構造体設定文」を記述できます。以下に、この例を示します。
R ::=
{
.P ::=
{
.A = 1;
.B = 2;
}
.Q = 0;
}
これを実行すると、デフォールトスコープ内に、構造体 R が生成され、そのメンバーが、
箱 P と箱 Q になります。箱 P は、構造体で、そのメンバーが、箱 A と箱 B です。
「メンバー設定部」には、任意の実行文が記述できますが、ラベルは設定できません。
また、ここには、関数定義も記述できません。以下に、「メンバー設定部」に、for文を
使った例を示します。
::G ::=
{
for( i = 0 ; i < 10 ; i++ )
.A[i] = 0;
}
これを実行すると、グローバルスコープ内に、構造体 G が生成され、そのメンバーには、
連想配列 A[0], A[1] ... A[9] が、登録されます。なお、ここで、使用している変数 i
には、スコープ規定演算子が付いていないので、変数 i は、デフォールトスコープ内の
変数になります。決して、構造体のメンバーではありません。
今までの例では、「構造体設定文」の実行時点で、「構造体名」の対象となる箱が存在
していませんでした。その場合、既に述べた通り、その構造体箱は、新規に生成されます。
「構造体設定文」の実行時点で、「構造体名」の対象となる箱が存在している場合、事情
が若干異なります。まず、「構造体名」の対象の箱が「箱を入れる箱」でない場合、その
箱は、無条件に、空の「箱を入れる箱」に変更されてから、新規生成された場合と同様の
構造体設定が行なわれます。一方、その対象の箱が「箱を入れる箱」の場合、その箱には
無条件に構造体としての属性が設定され、構造体のメンバー設定は、現状への追加になり
ます。例えば、前述の構造体 S が、前述の通り設定されている状態で、
S ::=
{
.C = 3;
.D = 4;
}
を実行すると、構造体 S のメンバーに、箱 C と箱 D が追加されます。もともとあった
箱 A と箱 B は、そのまま存続しています。
構造体内の各メンバーは、生成された順に登録されていきます。これは、構造体の箱に
限ったことではなく、一般の「箱を入れる箱」の場合でも同様です。この原則によって、
例えば、上述の構造体 S 内の各メンバーの登録順は、箱 A, B, C, D となります。この
登録順は、次節で説明する構造体のバイナリー構成を扱う時に、考慮する必要があります。
●入出力時のバイナリー構成
構造体内の各メンバーには、「入出力形式」を設定できます。これによって、構造体内
のデータを、ファイルやメモリーバッファに入出力する際のバイナリー構成(バイト構成)
を、厳密に規定できます。そうすれば、構造体内の各メンバーを逐一入出力しなくても、
構造体内の全データをまとめて、希望のバイナリー構成で、入出力することができます。
「入出力形式」の設定は、システム組み込みの後置型(リレー型)関数で行ないます。
これに関しては、「入出力形式」の章で詳しく説明します。ここでは、これを使った例を
示します。
まず、簡単な例として、画面上の点の位置( X, Y ) を表わす構造体を示します。
POINT ::=
{
.X 'LONG; // 点の X 座標
.Y 'LONG; // 点の Y 座標
}
ここで、'LONG は、対象の変数の入出力形式を「符号有 32-bit 整数」に設定します。
対象の変数がなければ、新規に生成します。これによって、構造体 POINT 内のメンバー
X, Y が生成され、その入出力形式が「符号有 32-bit 整数」になります。
この構造体 POINT を、ファイルやメモリーバッファへ出力する際には、
.X が 4 バイト( 32-bit )
.Y が 4 バイト( 32-bit )
の順で、合計 8 バイト( 64-bit )のバイナリーデータが書き込まれます。
また、入力する際には、この順で、このサイズのバイナリーデータが取り込まれます。
次に、Windows プログラミングではお馴染みの MSG 構造体を例に使います。この構造
体は、C++ 言語で、以下のように表わされます。
struct MSG // スレッドのメッセージ キューからのメッセージ情報
{
HWND hwnd; // メッセージを受け取るウィンドウのハンドル
UINT message; // メッセージ番号
WPARAM wParam; // 付加情報(1)
LPARAM lParam; // 付加情報(2)
DWORD time; // メッセージがポストされた時間
POINT pt; // カーソル位置(スクリーン座標系)
};
ここで、HWND, UINT, WPARAM, DWORD は、どれも「符号無 32-bit 整数」で、LPARAM は、
「符号有 32-bit 整数」です。また、 POINT は、上例と同じ構成の構造体です。
この MSG 構造体に相当する本言語の構造体の表記は、以下のようになります。
MSG ::=
{
.hwnd 'ULONG;
.message 'ULONG;
.wParam 'ULONG;
.lParam 'LONG;
.time 'ULONG;
.pt = POINT;
}
ここで、'ULONG は、'LONG と殆ど同じですが、入出力形式が「符号無 32-bit 整数」に
なります。また、.pt は、上例の構造体 POINT のコピーが代入されています。
この構造体のデータを、メモリーバッファへ書き出せば、C++ 言語での構造体のデータと
同じバイト構成になります。また、その構成のデータが格納されているメモリーバッファ
から、この構造体へ読み込めば、各メンバーに、対応する値が設定されます。この機能は、
Windows のAPIを使ってプログラミングする時等に、必要になります。
次に、ファイルのレコードの入出力用に構造体を使う例を示します。ここでは、以下の
ような構成のレコード( 48 バイト )があるとします。
個人情報 |
氏名 | 生年月日 | 性別
TD> | 血液型 | 電話番号 |
姓 | 名 | 年 | 月 | 日 |
文字10桁 | 文字10桁 | 数字4桁 | 数字2桁 | 数字2
桁 | 文字2桁 | 文字2桁 | 文字16桁 |
このレコードの入出力を対象にした構造体は、以下のような設定になります。なお、今回
は、識別名に日本語を使っています。
個人情報 ::= {
.氏名 ::= {
.姓 'C(10);
.名 'C(10);
}
.生年月日 ::= {
.年 'I(4);
.月 'I(2);
.日 'I(2);
}
.性別 'C(2);
.血液型 'C(2);
.電話番号 'C(16);
}
ここで、'C(n) は、対象の変数の入出力形式を「文字 n 桁(半角単位)」に設定します。
'I(n) は、対象の変数の入出力形式を「数字 n 桁」に設定します。両者とも、対象変数
がなければ、新規に生成します。
この構造体にデータを設定するには、
個人情報.氏名.姓 = "山田";
個人情報.氏名.名 = "太郎";
個人情報.生年月日.年 = 1964;
・・・・・
のように、各メンバーごとに逐一、その値を代入することもできますが、以下のように、
まとめて設定できます。これについては、次節で説明します。
個人情報 =
{ { "山田", "太郎" }, { 1964, 5, 3 }, "男", "O", "03-4567-8888" };
この「個人情報」のデータを、上記のレコード構成で、ファイルに格納するには、例えば、
以下のようにします。なお、格納先のファイル名は「Persons.dat」としています。また、
ファイル関連の操作は、「ファイル操作」の章で詳しく説明しています。
file = ::File.Open( "Persons.dat", "out" );
file.Write( 個人情報 );
ここで、「個人情報」の各メンバーには、入出力形式が設定されているので、
file.Write( 個人情報 );
とするだけで、構造体内の全データをまとめて、希望のバイナリー構成で、書き込むこと
ができます。続いて、もう3件、同様に格納して、ファイルを閉じます。
個人情報 = { { "春川","花子" }, { 1978, 3,21 }, "女", "A", "012-345-6789" };
file.Write( 個人情報 );
個人情報 = { { "花岡","実太" }, { 1950,10,10 }, "男", "B", "098-765-4321" };
file.Write( 個人情報 );
個人情報 = { { "橘","カオル" }, { 1999, 9, 9 }, "女", "AB","077-888-9999" };
file.Write( 個人情報 );
file.Close();
この実行によって、4件の「個人情報」レコードが格納されたファイル「Persons.dat」
が作成されます。
今度は、このファイルを読み出して、各「個人情報」の内容をプリントするプログラムを
書いてみます。
個人情報 ::= {
〜〜 ここは前記と同じ内容になる 〜〜
}
size = 個人情報 'size;
file = ::File.Open( "Persons.dat", "in" );
while( file.Read( 個人情報 ) == size )
{
do 個人情報 'enum with v { print v, -; };
print;
}
file.Close();
ここで、ファイル内の1レコードの読み出しは、単に、
file.Read( 個人情報 )
としているだけです。これで、「個人情報」の各メンバーには、レコード構成内の各対応
するデータ部の値が設定されます。ちなみに、この式の値は、このファイル操作によって
読み出されたデータのバイト数になります。その値が、「個人情報」の構造体のサイズと
等しければ、正常に読み出せたということになります。ファイルの終端では、等しくなら
ないので、while ループから抜け出します。
なお、'size は、対象の箱( この場合「個人情報」の構造体の箱 )の総データサイズを
返すシステム組み込みのリレー型関数です。従って、個人情報 'size は 48 になります。
また、'enum は、対象の箱の階層構造を深さ優先で辿っていって、各要素を順に指定関数
(この場合、do-with 式のブロック関数)に渡すシステム組み込みのリレー型関数です。
ここでのブロック関数は、渡された要素の内容を、コンマ付き無改行でプリントするだけ
です。このプログラムを実行すると、結果は、以下の通りです。
山田, 太郎, 1964, 5, 3, 男, O, 03-4567-8888,
春川, 花子, 1978, 3, 21, 女, A, 012-345-6789,
花岡, 実太, 1950, 10, 10, 男, B, 098-765-4321,
橘, カオル, 1999, 9, 9, 女, AB, 077-888-9999,
また、同ファイル( Persons.dat )から、ある条件に合致する個人情報を抽出して、
プリントするプログラムは、以下のようになります。なお、ここでは、抽出条件として、
3月生まれの女性にしています。また、ファイルオープン時のエラーチェックも付け加え
ています。
個人情報 ::= {
〜〜 ここは前記と同じ内容になる 〜〜
}
size = 個人情報 'size;
if(( file = ::File.Open( "Persons.dat", "in" )) == null )
{
print "個人情報のファイルがありません!";
return;
}
while( file.Read( 個人情報 ) == size )
{
if( 個人情報.生年月日.月 == 3 &&
個人情報.性別 == "女" )
{
do 個人情報 'enum with v { print v, -; };
print;
}
}
file.Close();
return;
このプログラムの実行結果は、以下の通りです。
春川, 花子, 1978, 3, 21, 女, A, 012-345-6789,
最後に、注意事項を述べておきます。ファイルやメモリーバッファから入力する際には、
必ず、各箱に「入出力形式」が設定されている必要があります。もし、設定されていなけ
れば、実行時にエラーになります。一方、出力する際には、必ずしも、「入出力形式」が
設定されていなくても構いませんが、その場合、その箱の内容に応じて、デフォールトの
入出力形式が適用されます。なお、入力の際にデフォールトの入出力形式を設けていない
のは、それが無意味か不適切になる場合が多いからです。
構造体の各メンバーが、ファイルやメモリーバッファに、入出力される順番は、基本的
に、登録順で、階層深化が優先されます。つまり、まず、最外箱内直下の各箱が登録順に
対象になります。その対象になった際、それが「値を入れる箱」であれば、それに対する
入出力が行なわれます。一方、それが「箱を入れる箱」であれば、その箱内直下の各箱が
登録順に対象になります。その対象になった際は、先程と同様です。各「箱を入れる箱」
内において、最後の対象の箱が終ると、その「箱を入れる箱」の次の箱に進みます。これ
が、対象の箱がなくなるまで繰り返されます。
構造体の各メンバーが一旦決まった後に、追加/削除を行なって、入出力する場合は、
この順番を配慮しておく必要があります。
●構造体へのデータ設定
構造体へまとめてデータを設定するのは、連想配列へ初期化データ列を設定するのと、
基本的に同じです。そのため、以下の説明では、連想配列への初期化データ列の設定が、
既に理解されていることを前提にしています。
まず、階層がフラットな場合ですが、例えば、
S ::=
{
.A 'LONG;
.B 'LONG;
.C 'LONG;
}
の構造体に対して、
S = { 1, 2, 3 };
とすると、S 内のメンバー A, B, C は、それぞれ、1, 2, 3 に設定されます。
波括弧 { } のブロック内の各要素は、省略できます。その場合、その省略要素に対応
するメンバーは、設定されません。また、当ブロック内の各要素の個数の方が、構造体の
メンバーの個数よりも少ない場合、不足分の要素に対応するメンバーは、設定されません。
例えば、上記の構造体 S に対して、
S = { , -2 };
とすると、S 内のメンバー B は、-2 に設定されますが、A, C は、設定されないので、
その値は、それぞれ、1, 3 のままです。
一方、波括弧 { } のブロック内の要素の個数の方が、構造体のメンバーの個数よりも
多い場合、過剰分の要素に対応するメンバーは、新規に登録されます。その時の登録名は、
そのメンバーの登録順に対応する連想名になります。この登録順は 0 から 1 ずつ増えて
いく整数です。これを i とすると、それに対応する連想名は、[i] になります。例えば、
上記の構造体 S に対して、
S = { 10, 11, 12, 13, 14 };
とすると、S 内のメンバー A, B, C は、それぞれ、10, 11, 12 に設定されますが、過剰
要素があるので、その分に対応するメンバー [3], [4] が新規に追加されて、それぞれの
値が、13, 14 に設定されます。つまり、S[3], S[4] の値は、それぞれ、13, 14 です。
次に、構造体の中にさらに構造体がある場合のデータ設定について説明します。例えば、
T ::=
{
.A 'LONG;
.B 'LONG;
.C ::=
{
.X 'LONG;
.Y 'LONG;
}
}
の構造体に対して、
T = { 1, 2, { 3, 4 } };
とすると、T 内のメンバー A, B, C は、それぞれ、1, 2, { 3, 4 } に対応しているので、
T.A = 1;
T.B = 2;
T.C = { 3, 4 };
として、設定されます。ここで、T.C は、構造体でその中の構造はフラットなので、その
中のメンバーは、上述の通り設定されます。つまり、T.C.X, T.C,Y は、それぞれ、3, 4
に設定されます。
このように、構造体の中にさらに構造体がある場合には、波括弧 { } のブロックを、
入れ子にすることによって、設定できます。この場合、入れ子になったブロック内での各
要素の省略や過不足に関しては、上述と同様です。しかしながら、構造体でないメンバー
にブロックが対応していたり、逆に、構造体であるメンバーにブロックが対応していない
と、構造体の構造が変わってしまうので、注意が必要です。例えば、
T = { 1, { 2, 3 }, 4 };
とすると、T 内のメンバー A, B, C は、
T.A = 1;
T.B = { 2, 3 };
T.C = 4;
として、設定されてしまいます。この場合、T.B の箱の中に [0] と [1] の連想名の新規
メンバーが登録され、それぞれ、 2 と 3 の値に設定されます。つまり、T.B[0], T.B[1]
は、それぞれ、2, 3 になります。一方、T.C は、元は構造体でしたが、この設定よって、
整数値 4 を格納する箱に変わってしまいます。その際、T.C.X と T.C,Y は、破棄されて
います。なお、この場合の T.B は、構造体としての属性を持っていません。
●任意関数のメンバー関数としての使用
本言語では、任意の関数を、構造体のメンバー関数として使用できます。例えば、以下
のような関数があったとします。
function ShowMember()
{
print .A, .B ;
}
この関数は、メンバースコープ内にある変数 A と変数 B の内容をプリントします。一方、
以下のような構造体があったとします。
S ::=
{
.A = 1;
.B = 2;
}
さて、この構造体のメンバー関数として、上の関数を適用するには、以下のようにします。
S.[ ShowMember ]();
これを実行すると、以下の通りプリントされます。
1, 2
また、以下のような別の構造体があったとします。
T ::=
{
.A = -1;
.B = -2;
.C = -3;
}
これは、前の構造体 S とは、特に依存関係はありませんが、この構造体 T に対しても、
同様に、同関数を適用できます。
T.[ ShowMember ]();
これを実行すると、以下の通りプリントされます。
-1, -2
次に、以下のような関数があったとします。
function MultMember( n )
{
.A *= n;
.B *= n;
}
これは、メンバースコープ内にある変数 A と変数 B の値を n 倍する関数です。これを、
先程の構造体 S と構造体 T に適用して、各メンバーの値ををプリントする例を、以下に
示します。
S.[ MultMember ]( 10 );
T.[ MultMember ]( 10 );
S.[ ShowMember ]();
T.[ ShowMember ]();
これを実行すると、以下の通りプリントされます。
10, 20
-10, -20
関数への参照を使っても、構造体のメンバー関数として、適用できます。例えば、先程
の続きで、
R := MultMember;
S.[ R ]( 10 );
T.[ R ]( 10 );
R := ShowMember;
S.[ R ]();
T.[ R ]();
を実行すると、以下の通りプリントされます。
100, 200
-100, -200
メンバー関数としてコールされた関数の中でも、任意の関数を同メンバーの関数として
コールできます。例えば、
function MultAndShow( n )
{
.[ MultMember ]( n );
.[ ShowMember ]();
}
この関数は、実行時点でのメンバースコープに対するメンバー関数として、つまり、同じ
構造体に対するメンバー関数として、関数 MultMember と ShowMember をコールします。
この関数 MultAndShow を、先程の続きの構造体 S と構造体 T のメンバー関数として、
以下のように、コールします。
S.[ MultAndShow ]( 100 );
T.[ MultAndShow ]( 100 );
これを実行すると、以下の通りプリントされます。
10000, 20000
-10000, -20000