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 バイト )があるとします。

個人情報
氏名生年月日性別血液型電話番号
文字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