MikoScript3 言語仕様
構造体と構造化リスト
「構造体」とは、あるまとまったデータ構造(或いはデータ構成)を表わすものです。
本言語では、構造体は、「複合箱」になるので、その複合箱の構造が、その構造体の構造
に対応します。そのため、構造体の設定は、構造体の雛型を定義するのではなく、構造体
を構成する実体、つまり、構造体の複合箱の中身を、実行文によって、構成していくこと
になります。かくて、構造体の構造は、プログラムの実行時に動的に自由に変化できます。
ちなみに、コンパイラ言語等では、通常、構造体の構造は、プログラムのコンパイル時に
確定されてしまい、実行時には変化できません。
「構造化リスト」とは、HTML や XML などの構造化データ、あるいは、LISP 言語等の
リストのようなデータを、簡便な表記で、設定できるようにした構造体です。
●構造体の設定
構造体の複合箱の中にある箱を、その構造体の「メンバー」と言います。つまり、構造
体は、メンバーの集まりで構成されます。構造体の設定では、そのメンバーを決めること
が主体になります。
構造体は、通常、「構造体設定文」の実行によって、設定します。この構文の書式は、
以下のようになります。
構造体名 ::=
{
メンバー設定部
}
ここで、「構造体名」には、設定対象の構造体の名前を規定します。この名前は、その
構造体の複合箱を代表する最外の箱の名前です。これは、単一の識別名だけでなく、一般
に「箱の経路」を規定できます。この箱の経路の基点は、スコープ規定演算子によって、
規定できます。それは通常の箱名の場合と同じです。構造体名に連想名を含む場合、その
インデックスには変数が使えます。
::= という記号が、「構造体名」の直後にあります。これは、一見、演算子のようです
が、あくまで構文構成上の記号で、文法的には、演算子ではありません。本構文は、この
ような記号を使わないで、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
●構造化リスト
HTML や XML などの構造化データ、あるいは、LISP 言語等のリストのようなデータは、
今までに説明した通常の構造体でも、充分扱えます。ただ、そのデータ構造の表記の段階
において、不便さと不自然さがあります。例えば、次のような HTML のテキストがあると
します。
<html>
<head>
<title>構造化データ</title>
</head>
<body>
<h1>表記言語</h1>
<table>
<tr><td>1</td><td>マークアップ言語</td></tr>
<tr><td>2</td><td>LISP 言語</td></tr>
<tr><td>3</td><td>MikoScript 言語</td></tr>
</table>
</body>
</html>
通常の構造体で、これを表わすと以下のようになります。なお、1つのスコープ内で同じ
名前の箱は2コ以上登録できないので、td と tr は通し番号を付加して各々別名にして
います。また、数値や文字列は、とにかく箱に格納しないと保持されないので、ここでは、
._ という名前の箱に格納しています。
html ::= {
.head ::= {
.title ::= { ._ = "構造化データ"; }
}
.body ::= {
.h1 ::= { ._ = "表記言語"; }
.table ::= {
.tr_1 ::= { .td_1 ::= { ._ = 1; }; .td_2 ::= { ._ = "マークアップ言語"; } }
.tr_2 ::= { .td_1 ::= { ._ = 2; }; .td_2 ::= { ._ = "LISP 言語"; } }
.tr_3 ::= { .td_1 ::= { ._ = 3; }; .td_2 ::= { ._ = "MikoScript 言語"; } }
}
}
}
「構造化リスト」では、これをもっと簡便で自然に表記できます。それを以下に示します。
html {
head {
title { "構造化データ" }
},
body {
h1 { "表記言語" },
table {
tr { td { 1 }, td { "マークアップ言語" } },
tr { td { 2 }, td { "LISP 言語" } },
tr { td { 3 }, td { "MikoScript 言語" } },
}
}
}
さて、このように表記したデータが、実際にどのような構造になっているかですが、それを見る
ために、次のスクリプトを実行してみます。
do html'enum with p { print " "'rep( p'level ) : ( p'cbox? ? p'name : p ); };
すると、以下のようにプリントされます。なお、ここで、tr と td に番号が付いている
ところがありますが、これに関しては、後で説明します。
head
title
構造化データ
body
h1
表記言語
table
tr
td
1
td$1
マークアップ言語
tr$1
td
2
td$1
LISP 言語
tr$2
td
3
td$1
MikoScript 言語
さて、構造化リストは、「構造化リスト文」の実行によって設定します。この構文の
書式は、以下のようになります。
構造箱 { 項目指定 }
ここで、「構造箱」は、その構造化リストを格納する箱を指定します。これは、単一の
識別名だけでなく、「箱の経路」や「箱を特定する式」が規定できます。この「箱の経路」
の基点は、スコープ規定演算子で規定できます。例えば、グローバル・スコープなら、
::箱名 { 項目指定 }
のようにできます。また、「箱を特定する式」には参照や関数などを使っても構いません。
これらは通常の箱名の場合と同じです。
「項目指定」には、0個以上の「項目」を指定します。「項目」が複数個ある場合は、
各項目を「,(コンマ)」で区切ります。つまり、
構造箱 { 項目, 項目, 項目, ・・・ }
のようになります。これで、その構造箱の中に、これらの各項目の箱が設定されます。
この「項目」には、次の (1) 〜 (8) のどれかを指定します。
定数 ・・・・・ (1)
( 式 ) ・・・・・ (2)
{ 項目指定 } ・・・・・ (3)
識別名 ・・・・・ (4)
識別名 = 式 ・・・・・ (5)
識別名 := 式 ・・・・・ (6)
識別名 <- 式 ・・・・・ (7)
識別名 { 項目指定 } ・・・・・ (8)
(1) では、その項目の箱に、その定数値(整数、実数、文字列、null)が設定されます。
(2) では、その項目の箱に、その式の評価値(または参照)が設定されます。
(3) では、その項目の箱に、そのサブ構造化リストが設定されます。
(4) では、その識別名の項目箱が、「空箱」に設定されます。
(5) では、その識別名の項目箱に、その式に対する複製代入が行なわれます。
(6) では、その識別名の項目箱に、その式に対する参照代入が行なわれます。
(7) では、その識別名の項目箱に、その式に対する移動代入が行なわれます。
(8) では、その識別名の項目箱に、その「項目指定」のサブ構造化リストが設定されます。
(1) 〜 (3) の場合、その項目の箱は、「$i」( i = 0,1,2,... ) という特殊な名前に
なります。つまり、(1) 〜 (3) の項目の箱は、最初は、$0 という名前になり、
次は $1、それ以降は、$2, $3, $4 ... となります。
(2) の場合、丸括弧は省略できません。その項目の箱へ代入されるのは、
・式の評価値が、整数値、実数値、定文字列、null の場合、その値
・式が、複合箱、関数箱(匿名関数定義も含む)等の場合、その箱への参照
となります。
また、式が関数コールで、その返値が多値の場合、その個数分の箱へ、各値が
代入されます。例えば、$2 の箱から3値を代入する場合、$2, $3, $4 の3箱へ
代入されます。
(4) 〜 (8) の場合、同じ「識別名」の項目が既に設定されていると、今回新たに追加
される項目の箱名は「その識別名 $ 通番」に変更されます。例えば、abc という
名前の項目箱が既にある時に、同じ「識別名」の項目箱が追加されると、その項目
の箱名は、abc$1 になります。さらに同名の項目箱が追加されると、その箱名は、
abc$2 になります。以降同様に、abc$3, abc$4, ... となります。
ちなみに、先ほどの tr と td に番号が付いていたのは、このためです。
(2),(5),(6),(7) の「式」内の箱名のスコープは、次のようになります。
・スコープ規定演算子のない箱名は、デフォールトスコープ、つまり、現実行中の
関数のローカルスコープになります。ちなみにその構造箱内のローカルスコープ
になるわけではありません。
・メンバースコープの箱名( .箱名 )の場合、その構造箱内のローカルスコープに
なります。例えば、X1 と X2 という名前の項目が既に設定されていて、それらの
和を今回の項目の値にするには、( .X1 + .X2 ) のようにします。また、$0, $1
等の特殊な名前の箱に対しても同様に、( .$0 - .$1 * 10 ) のようにします。
・それ以外のスコープ規定演算子の付く箱名は、通常通りです。
次に、構造化リストの実用時の参考になりそうな例を示します。
≪ 例1 ≫ SVG (Scalable Vector Graphics) の表記に対応した例
svg { { width="128mm", height="96mm", viewBox { 0, 0, 640, 480 } },
rect { x=2, y=2, width=637, height=478, fill=0, stroke="green" },
circle { cx=300, cy=100, r=160, fill="red" },
text { x=320, y=180, font_family="Georgia", font_size=18,
"This is a test to draw SVG." }
}
do svg'enum with p { print " "'rep( p'level ) : p'name : " = " : p ; };
これを実行すると、以下のようにプリントされます。
$0 = <structure: $0>
width = 128mm
height = 96mm
viewBox = <structure: viewBox>
$0 = 0
$1 = 0
$2 = 640
$3 = 480
rect = <structure: rect>
x = 2
y = 2
width = 637
height = 478
fill = 0
stroke = green
circle = <structure: circle>
cx = 300
cy = 100
r = 160
fill = red
text = <structure: text>
x = 320
y = 180
font_family = Georgia
font_size = 18
$0 = This is a test to draw SVG.
≪ 例2 ≫ 識別名に日本語を使った例
レシピ {
料理 = "ゆでたまごのエコな作り方",
調理時間 = "16分",
材料 { "たまご", "3個" },
材料 { "塩", "少々" },
作り方 {
"小鍋に水を入れる(深さが 1cm になる程度でよい)",
"たまご3個と塩を少々入れる",
"強火にかけて水が沸騰するまで待つ",
"弱火にして、鍋にふたをしてから、5分間待つ",
"火を消して、鍋にふたをしたまま、10分間待つ",
"たまごを水で冷やして、出来上がり",
},
補足 = function( s ) { print s: "塩は特に入れなくても構いません!"; }
}
do レシピ'enum with p { print " "'rep( p'level ) : p'name : " = " : p ; };
レシピ.補足( " ※ " );
これを実行すると、以下のようにプリントされます。
料理 = ゆでたまごのエコな作り方
調理時間 = 16分
材料 = <structure: 材料>
$0 = たまご
$1 = 3個
材料$1 = <structure: 材料$1>
$0 = 塩
$1 = 少々
作り方 = <structure: 作り方>
$0 = 小鍋に水を入れる(深さが 1cm になる程度でよい)
$1 = たまご3個と塩を少々入れる
$2 = 強火にかけて水が沸騰するまで待つ
$3 = 弱火にして、鍋にふたをしてから、5分間待つ
$4 = 火を消して、鍋にふたをしたまま、10分間待つ
$5 = たまごを水で冷やして、出来上がり
*3 = <function: *3>
※ 塩は特に入れなくても構いません!
次に、「構造化リスト」内に、項目を追加する方法を説明します。例えば、
Q { abc { 1 }, abc { 2 }, abc { 3 } };
のようにして、構造化リスト Q を生成したとします。このあと、
Q { abc { 4 }, abc { xyz { 5 } }, abc { xyz { uvw{ 6 } } }, };
を実行すると、Q 内には、これらの項目が追加されます。さらに、
Q.abc$0 { 11, 111 };
Q.abc$1 { 22, 222 };
を実行すると、結局、構造化リスト Q 内は、以下のようになります。
abc
1
11
111
abc$1
2
22
222
abc$2
3
abc$3
4
abc$4
xyz
5
abc$5
xyz
uvw
6
●同属名
同じ名前の箱を、同じスコープ内に、複数個入れることは、原理的にできませんが、
上記の「構造化リスト」では、「識別名 $ 通番」によって、それに近いことができる
ようになっているということは、既に述べた通りです。
「同属名」は、その「識別名 $ 通番」を若干拡張したもので、それ以外の用途にも
使えるようになっています。
同属名の表記は、簡易的には、次のようになります。
A$i
ここで、A は、任意の識別名、i は、0以上の整数定数です。例えば、
name$1 hobby$2 robot$8
などが、同属名の簡易表記です。
同属名のもう少し汎用的な表記は、次のようになります。
A$(i)
ここで、A は、任意の識別名、i は、評価値が、整数、実数、あるいは、文字列になる
任意の式です。例えば、
title$(2) size$( N++ ) home( "new" ) 重量$( x + y ) 色彩$( 4.5 )
などは、同属名の表記です。
なお、上記のどちらの表記でも、i が 0(整数値)の時は、元の識別名そのものと同じ
になります。例えば、X$0 と X$(0) は、X と同じです。
同属名の表記の $ の左側の識別名を連想配列名にすることはできません。例えば、
X[1]$2 Y[i]$(n)
などは、文法エラーになります。
$ の直後の丸括弧内の数値が同じでも、データ型が異なれば別の箱になります。例えば、
Z$( 1 ) Z$( 1.0 ) Z$( "1" )
は、それぞれ、データ型が、整数、実数、文字列なので、別の箱です。
同属名と連想配列は、似ているところもありますが、根本的な違いもあります。以下に
両者の比較表を示します。
同属名 | 連想配列 | 形 態 |
A$1 | 不可 | 簡易表記 |
A$(i) | A[i] | 1次元インデックスでのアクセス |
不可 | A[i][j] | 多次元インデックスでのアクセス |
1 | 2 | 要素の深さ( 'level で確認可 ) |
A'qs | A'data | インデックスなしでの要素の追加 |
同属名が、実際に内部的にはどのような箱名になっているのかは、リレー型関数 'name
を使えば分かります。例えば、
P$1 = 0;
Q$5'new!;
i = 123;
R$( i ) = 1.23;
S$( "abc" ) = null;
print P$1'name, Q$5'name, R$(i)'name, S$("abc")'name;
を、実行すると、
P$1, Q$5, R$123, S$abc
と、プリントされます。
同属名関連のリレー型関数があります。これらを次に説明します。
'qs 関数
書式: <対象箱>'qs
説明: <対象箱> の同属名の空箱を新規に生成します。その際に、
<対象箱> が不在なら、<対象箱> 自身を生成します。
<対象箱> が既存なら、その識別値が当スコープ内で最小の正値となる同属名
の箱を生成します。( 同属名の識別値に関しては、'qval を参照。)
なお、<対象箱>には、連想配列名は使えません。
返値: 新規生成した同属名の箱の参照を返します。
当箱を新規生成できない時は、null を返します。
用例: 次のスクリプトを実行すると、「<null>, 3, xyz」とプリントされます。
A'qs; // 空箱の A を新規生成
A'qs = 3; // 空箱の A$1 を新規生成して、3 を代入
A'qs = "xyz"; // 空箱の A$2 を新規生成して、"xyz" を代入
print A, A$1, A$2;
'qtop 関数
書式: <対象箱>'qtop
説明: <対象箱> の同属名の箱群のうち、先頭の箱の参照を返します。
補説: <対象箱> の同属名とは、その箱名自身、または、その箱名の後に $ と任意の
文字列が付加された箱名のことです。例えば、<対象箱> が A の場合、A$...
に一致する箱名です。(ここで、... は、任意の文字列)
なお、<対象箱> は既存でも不在でも構いませんが、連想配列名は使えません。
先頭の箱というのは、最初に生成された(生成時期が最も古い)箱のことです。
返値: 当箱の参照を返します。
当箱が見つからない(実在しない)時は、null を返します。
用例: 次のスクリプトを実行すると、「B = 0」,「B$1 = 1」,「B$2 = 2」の3行が
プリントされます。
( B'qs, B'qs, B'qs ) = ( 0, 1, 2 );
for( p := B'qtop ; p'ref? ; p := p'qnxt )
print p'name : " = " : p;
'qnxt 関数
書式: <対象箱>'qnxt
説明: <対象箱> の次の同属名の箱の参照を返します。
当箱がない時は、null を返します。
なお、<対象箱> は、実在する箱でないといけません(連想配列名は不可)。
用例: 次のスクリプトを実行すると、「C = 0」,「C$1 = -1」,「C$2 = -2」の3行が
プリントされます。
( X.C, X.C$1, X.C$2 ) = ( 0, -1, -2 );
for( p := X.C'qtop ; p'ref? ; p := p'qnxt )
print p'name : " = " : p;
'qval 関数
書式: <対象箱>'qval
説明: <対象箱> の同属名の識別値を返します。
同属名の識別値というのは、同属名の一般的な表記「A$(i)」(上記参照)での
i の値になります。これには、整数、実数、文字列の場合があります。
なお、<対象箱> がない時は、null を返します。
<対象箱> には、連想配列名は使えません。
用例: 次のスクリプトを実行すると、「D, 10, 0」,「D$1, 11, 1」,「D$akb, 12, akb」
の3行がプリントされます。
D = 10;
D$5 = 11;
D$( "akb" ) = 12;
for( p := D'qtop ; p'ref? ; p := p'qnxt )
print p'name, p, p'qval;