Moai+Easter Advanced Manual
ご案内 Moaiエンジン CustomBoyエンジン HowToコンパイル Moai CGI Developers Raraku言語
Raraku言語マニュアル(前編) Raraku言語マニュアル(後編) Raraku Advanced document
はじめに
この記事は、Raraku言語マニュアル後編です。目次
- ヒアドキュメント
- ビット演算子
- tight文とimmut文とrefer文
- ダブルモディファイア
- final文
- functype文とコールバック関数
- 無名関数
- lambda式
- ファイル入出力とネイティブオブジェクト
- defer文
- ヘッダファイルとリンケージ
- 型のキャストと暗黙変換
- システムインターフェース
- goto文とラベル文
- モジュールとトレイト
- 大域脱出とtry文
- ジェネリクス
- マクロ
- Appendix
ヒアドキュメント
ヒアドキュメントの基本
ヒアドキュメントとは「@」で開始する文字列リテラルの記法です。 これについては「文字列」のセクションでも一部紹介しましたが、ここで改めて正確な書式を示すと以下のようになります。
「@」「指定子設定部」「識別子パターン」「開始FlexibleBracket」「'」中身の文字列「'」「終了FlexibleBracket」「識別子パターン」
上記のうち、「@」と「'」については必須です。 「'」と「'」で囲まれた「中身の文字列」が文字列リテラルの実際の値となります。 それ以外の「指定子設定部」「識別子パターン」「開始FlexibleBracket」「終了FlexibleBracket」は適宜省略可能です。 すべて省略した場合は、以下のように「文字列」のセクションでも紹介した「@'」と「'」で囲う記法になります。
varia s = @'Hello.'
Rrk_print( s )
最初の「識別子パターン」を省略した場合、最後の「識別子パターン」も省略しなければなりません。 最初の「識別子パターン」を指定した場合、最後の「識別子パターン」と同じでなければなりません。 例えば以下は識別子パターンとして「__here__」を指定しています。
varia s = @__here__'Hello.'__here__
Rrk_print( s )
このとき中身となる値の文字列には「'__here__」という文字列が含まれていてはいけません。 これがこのヒアドキュメントの終了を示す印になるからです (「'」や「__here__」が単独で含まれるのであれば構いません)。 もしも「'__here__」が値の文字列に含まれる場合は、「__here__」以外の識別子パターンを適当に指定すればよいでしょう。
上の例では「__here__」という識別子を使いましたが、
ユーザはどのような識別子を指定しても構いません。
またこれは同じファイルに宣言されている変数や関数などの名前と被ってもかまいません。
ただしそれが「識別子」としての条件を満たす必要はあります。 Rarakuでの「識別子」とは、最初の文字は「_」またはアルファベット(数字はNG)、 二文字目以降は「_」またはアルファベットまたは数字からなる文字列です。
ただしそれが「識別子」としての条件を満たす必要はあります。 Rarakuでの「識別子」とは、最初の文字は「_」またはアルファベット(数字はNG)、 二文字目以降は「_」またはアルファベットまたは数字からなる文字列です。
「開始FlexibleBracket」とは「[」で始まり、その直後に「=」を0個以上並べたものです。 「終了FlexibleBracket」とは「]」で終わり、その直前に「=」を0個以上並べたものです。 「開始FlexibleBracket」と「終了FlexibleBracket」に指定する「=」の個数は必ず同じでなければなりません。 例えば以下は「開始FlexibleBracket」と「終了FlexibleBracket」において「=」を2個並べています。
varia s = @[=='Hello.'==]
Rrk_print( s )
これはLuaにおける文字列リテラルの記法を参考にしたものです。
また以下は「開始FlexibleBracket」と「終了FlexibleBracket」において「=」を0個並べています。 つまり「文字列」のセクションで紹介した「@['」と「']」で囲う記法になります。
varia s = @['Hello.']
Rrk_print( s )
「開始FlexibleBracket」を省略した場合、「終了FlexibleBracket」も省略しなければなりません。
「識別子パターン」、「開始FlexibleBracket」、「終了FlexibleBracket」をすべて同時に指定することもできます。 例えば以下の通りです。
varia s = @__greeting__[='Hello. World.'=]__greeting__
Rrk_print( s )
識別子パターンにその文字列リテラルに関係する名前を付けることによって、
極簡単なコメントとしての役割をさせることもできます。
そのため、これらをすべて同時に指定可能な仕様となっています。
「指定子設定部」は「@」の直後に指定するもので、ヒアドキュメントの挙動をカスタマイズできます。 「指定子設定部」は正確には三つの設定部から構成され、その書式は以下のようになります。
「EmbedEscape設定部」「Indent設定部」「LineBreak設定部」
EmbedEscape設定部、Indent設定部、LineBreak設定部はどれも省略可能ですが、 指定する場合は必ずこの順番で指定しなければなりません(アルファベット順にE、I、Lと覚えてもよいでしょう)。 それぞれの意味については追々説明しますが、ざっくりと言うと EmbedEscape設定部は変数埋め込みとエスケープ文字を有効化するための設定、 Indent設定部は主に行頭の余計なインデントを削除/追加するための設定、 LineBreak設定部は主に生の改行コードを強制的に変換するための設定となります。
中身が複数行に渡るヒアドキュメント
ヒアドキュメントでは、中身が複数行に渡るものを記述できます。 例えば以下の通りです。
varia s = @__greeting__'
Hello.
World.
'__greeting__
Rrk_print( s )
上記の礼では「@__greeting__'」の直後に、最初の文字としていきなり生の改行コードがあります。 ここで一つ重要な注意ですが、Rarakuのヒアドキュメントでは一番最初の文字が生の改行の場合、それは無視されます。 ですから今回の場合、実際の値はその次に来る文字からになります。
一番最初の文字が生の改行ではない場合は字面通り解釈されます。
例えば上記でもし「@__greeting__'Hello.」と連続して続くならば、(一番最初の文字は「H」ですので)改行のカットは一切生じないことになります。
次の行では(字面ではわかり辛いですが)行頭にタブ文字が2つ連続し、その直後に「Hello.」と改行が続きます。 さらに次の行では行頭にタブ文字が1つあり、その直後に「World.」と改行が続きます。 最後の行では終了の印である「'__greeting__」が現れますのでヒアドキュメントの終了となります。
尚、一番最後の行の(生の)改行は字面通り含まれます。
上の例ですと「World.」とある部分と「'__greeting__」の間にある改行はそのまま残ることになります。
またもう一つ重要な注意ですが、上記のようにヒアドキュメントの中身の文字列を複数行に渡って記述した場合、 生の改行コードは強制的に「\n」とみなされます(たとえ生の改行コードが「\r\n」であっても「\n」に変換されるということです)。 つまり上記は「"」を使って以下のように書いたのと必ず同じになります。
varia s = (
" Hello.\n"
" World.\n"
)
Rrk_print( s )
これをもし、「\n」ではなく「\r\n」とみなしてほしい場合は、LineBreak指定部に「/"\r\n"」と指定します(「"」が必要です)。 例えば以下の通りです。
varia s = @/"\r\n"__greeting__'
Hello.
World.
'__greeting__
Rrk_print( s )
いずれにしてもこのコードを実行した結果は、外見上は以下のようになります。
Hello.
World.
ヒアドキュメントでは「一番最初の文字が改行の場合、それは無視される」と述べましたが、 もしも本当に一番最初に改行を付けたいなら、以下のように改行を2つ書く必要があります(つまり最初に空行が一つある形になります)。
const s = @__C_source_code__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
あるいはIndent設定部に「^」指定子を付ける方法もあります。
この指定子を付けた場合は空行を置く必要がありません。
例えば以下は上記と全く同じ結果となります。
const s = @^__C_source_code__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
Rarakuのヒアドキュメントにおいて、一番最初の改行は無視される仕様になっているのは、
実際の用途においてはそのようにした方が都合がいい場合が多いからです。
例えば中身の文字列として、次のようにプログラミング言語のソースコード(この例ではC言語ですが)を記述する場合を考えます。
この例ではパターン文字列として、__C_source_code__を指定しています。 そしてこのヒアドキュメントの字面上では、「@__C_source_code__'」の直後に最初の文字として改行があります。 このように書いた場合、多分ほとんどの人は最初の行(#include <stdio.h>)の前に余計な改行を入れて欲しくないと思うことでしょう。
尚、以下のように一番最初が改行コードではない場合は、特に何も加工は行われません。 つまり字面通りの値となります(以下の例では一番最初は「#」文字で始まっています)。
よって、「#include <stdio.h>」の前に余計な改行を入れて欲しくないならば上記のように書くこともできるのですが、 これでは最初の行がかなり見難くなってしまいます。 やはり「@__C_source_code__'」の次の行から「#include <stdio.h>」を書いた方が通常は見やすいと思われます。
以上のような理由で一番最初にもし改行コードがあるならそれを無視するような仕様になっています。
const s = @__C_source_code__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
この例ではパターン文字列として、__C_source_code__を指定しています。 そしてこのヒアドキュメントの字面上では、「@__C_source_code__'」の直後に最初の文字として改行があります。 このように書いた場合、多分ほとんどの人は最初の行(#include <stdio.h>)の前に余計な改行を入れて欲しくないと思うことでしょう。
尚、以下のように一番最初が改行コードではない場合は、特に何も加工は行われません。 つまり字面通りの値となります(以下の例では一番最初は「#」文字で始まっています)。
const s = @__C_source_code__'#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
よって、「#include <stdio.h>」の前に余計な改行を入れて欲しくないならば上記のように書くこともできるのですが、 これでは最初の行がかなり見難くなってしまいます。 やはり「@__C_source_code__'」の次の行から「#include <stdio.h>」を書いた方が通常は見やすいと思われます。
以上のような理由で一番最初にもし改行コードがあるならそれを無視するような仕様になっています。
ヒアドキュメントのエスケープ文字
ヒアドキュメントではデフォルトではエスケープ文字が無効になっています。 これにより中身の文字列値で「"」「'」「\」等を記述した場合は、その字面通りの値になります。 例えば以下をご覧下さい。
const s = @[=='"'abc\n\t\\'==]
Rrk_print( s )
上の例を実行すると以下のようになります。
"'abc\n\t\\
「\n」「\t」「\\」の部分は「\」と「n」と「t」が字面通りに表示されていることが確認できます。
「文字列」のセクションでも少し述べましたが、ヒアドキュメントにおいてエスケープ文字を有効にするには「EmbedEscape設定部」において 「,エスケープ文字」と指定します(カンマが必要です)。 エスケープ文字としては「$」、「%」、「&」の三つから選択できます(これらはASCIIコードにおける24、25、26です)。 これ以外の文字の指定は許可されません(「\」も指定不可です)。
この三つ以外が許可されない理由は、一つはASCIIコードにおける30以上の文字になると全角文字によって使われる可能性が出て来るからです。
例えば中国の文字コードであるGB18030では、ASCIIコードにおける30から39までの値が使われる可能性があります。ただし30から39の範囲にあるものは
記号文字ではなく「0」から「9」までの(文字としての)数字になります。
さすがに数字をエスケープ文字とはしない方がよいので、これについては最初から無視してよいでしょう。
3aから3fまでにある記号については全角文字の一部として使われる可能性はありません。ですから実質的なボーダーは40以上になるかと思います。
もう一つの理由は、あまりエスケープ文字として使える記号の種類を増やすと、テキストエディタでRarakuのコードを色づけハイライトする場合に その設定ファイルを記述するのが大変になると予想されるからです。
もう一つの理由は、あまりエスケープ文字として使える記号の種類を増やすと、テキストエディタでRarakuのコードを色づけハイライトする場合に その設定ファイルを記述するのが大変になると予想されるからです。
例えば「%」をエスケープ文字として指定したい場合、「EmbedEscape設定部」において「,%」と指定します。 この様子を以下に示します。
const s = @,%[=='"'abc%n%t%%'==]
Rrk_print( s )
上の例を実行すると以下のようになります。
"'abc
%
「%n」「%t」「%%」の部分は、それぞれ改行、タブ文字、「%」として表示されていることが確認できます。
エスケープ文字を有効にすると、中身の文字列値にヒアドキュメントの終了パターンを記述することができます。 上記の例ですと終了パターンは「'==]」ですが、この「'」と「]」の間に割り込む位置に「%」を挿入したものを中身に書くことで 「'==]」という文字列を含めることができます(つまりこの場合、「'%==]」、「'=%=]」、「'==%]」のいずれかです)。 例えば以下の通りです。
const s = @,%[=='aaa'=%=]bbb'==]
Rrk_print( s )
上の例を実行すると以下のようになります。
aaa'==]bbb
「%'==]」のように記述した場合(つまり「'」の直前に「%」を置いた場合)、この「%」はエスケープ文字としての意味を失って「%」がそのまま表示されます。 また直後の「'==]」は依然として終了パターンとして機能しますのでご注意ください。
ヒアドキュメントの変数埋め込み
ヒアドキュメントにおいても文字列の連結を行うことができます。 例えば以下の例では二つのヒアドキュメントが括弧内において連続しており、aaa と bbb が連結されて「aaabbb」がsに代入されます。
const s = ( @__here__'aaa'__here__ @__here__'bbb'__here__ )
Rrk_print( s )
この例では同じ識別子パターン「__here__」を最初のヒアドキュメントと次のヒアドキュメントでも使いまわしていますが、これは特に問題ありません。 また勿論、次のように「&」演算子を間に入れることによって連結させることもできます。 要するに通常の文字列リテラルと同じ要領です。
const s = @__here__'aaa'__here__ & @__here__'bbb'__here__
Rrk_print( s )
実行結果は上記のどちらも以下のようになります。
aaabbb
ヒアドキュメントと言えども文字列リテラルですから、
上記のようにそれが連続する場合、括弧や「&」をつけなくても特例として連結はできるのですが、
開始パターンが長い場合でしかも中身が何行にも渡る場合、連結の範囲がかなりわかりにくくなるため、
連結範囲全体を括弧で括るか、明示的に「&」を付けることをお勧めします。
また、以下のように複数行に渡って記述した場合も同じく連結されます。
const delimiter = "====\n"
varia s = ( @__here__'
hello,
'__here__
delimiter
@__here__'
world!
'__here__ )
Rrk_print( s )
実行結果は以下のようになります。
hello,
====
world!
上の例では「varia s」の行から「'__here__ )」とある行までが一つのvaria文です。 このようにすると(括弧の開始と終わりに注目すればよいとはいえ)一つの文とわかりにくいと思います。 このような場合、次に述べる変数埋め込みを使った方がよいかもしれません。
「文字列」セクションでも軽く述べましたが、Rarakuのヒアドキュメントでは、内部に(文字列リテラルの外部で宣言済みの)変数を埋め込み、その値で展開するむことができます。 そのためにはEmbedEscape設定部において、「$」を指定します(「,$」と指定した場合はエスケープ文字を「$」とする指定でしたが、 単に「$」と書いた場合、それは変数埋め込みのための指定となります)。 この場合、文字列リテラル内部で「${変数名}」というパターンが存在すると、その部分はすべて(宣言済みの)変数の値で展開されます。 例えば以下の通りです。
const x = 10
const y = 20
const str = @$'aaa${x}bbb${y}ccc'
Rrk_print( str )
上記の例では、ヒアドキュメントの開始パターンは「@$'」となっています(「$」を指定しています)。 文字列リテラル内の「aaa${x}bbb${y}ccc」の部分は、(ヒアドキュメントの外にある)変数x,yの値(今回は10,20)で置き換えられます。 すなわちこの部分は結果的に「aaa10bbb20ccc」と展開されます。
この記法を使うと一つ前の例は以下のように書けます。
const delimiter = "====\n"
varia s = ( @$__here__'
hello,
${delimiter}
world!
'__here__ )
Rrk_print( s )
上記では、開始パターンが「@$__here__'」に変更されています。 またこれにより、「${delimiter}」の部分が「====\n」という値に置き換えられます。 実行結果は先ほどと同じになります。
これで大分マシになったとは言え、やはり開始/終了パターンと中身の文字列の開始のカラム位置がすべて同じであるため、 わかりにくさは否めません。 そこでできれば以下のようにインデントして書きたいところですが、 そうすると中身の文字列にも余分なタブ文字が入ってしまいます。
const delimiter = "====\n"
varia s = ( @$__here__'
hello,
${delimiter}
world!
'__here__ )
Rrk_print( s )
Rarakuのヒアドキュメントでは、このようなインデント時に入る余分なタブ文字を消去する方法が比較的豊富に用意されています。 次の項ではそれについて説明します。
この展開はヒアドキュメントを「@$__here__'」のように「$」を指定して始めた場合のみ有効です。
「$」を指定せず、「@__here__'」などのようにして始めた場合は、上記の「${delimiter}」はそのまま字面通りの値となり、
delimiterの変数値(「====\n」)で展開されることはありません。
ちなみに「"」で囲った場合はこのような展開はそもそもサポートされません。
${...}の内部には、必ず式を書かなければなりません。
ここで式とは、数値リテラル、文字列リテラル、識別子、配列要素へのアクセス、構造体メンバへのアクセス、値を返すような関数の呼び出し等です。
それ以外のもの、例えば純粋な文やブロック、値を返さない関数の呼び出しなどを指定するとコンパイルエラーとなります。
一応、後のセクションで述べる無名関数やlambda式などを使えば、その中にブロックや文の羅列を置くことも可能ではありますが、
よほどの理由がない限りこれは避けた方がよいでしょう。
通常はあまり複雑でない式(例えば単独の識別子や、単なる配列要素へのアクセス、単なる構造体メンバへのアクセス、 単なる(値を返すような)関数呼び出し等)を指定することをお勧めします。 上記の「${delimiter}」が埋め込まれた例で考えますと、例えばもしここにdelimiterではなく、 宣言されていない別の変数名を指定した場合はコンパイルエラーとなりますが、 これは非常に単純な式ですので何が悪いのか瞬時にわかります。
しかしこの部分にあまりに複雑な式を記述すると、何か文法上の間違いを入れていざコンパイルエラーとなったときに、 ヒアドキュメント内部に埋め込まれている関係でエラーメッセージが混乱し、 どの部分がおかしいのかよくわからなくなるかもしれません(特にヒアドキュメントが複数行に渡る場合、 エラーメッセージで報告される行番号がヒアドキュメントの最後の行のものになることがあります)。
Rarakuでは${...}を内部的に「&」「(」「...」「)」「&」のように展開し、
残りのヒアドキュメントの部分を単なる文字列リテラルのトークンに分割します
(「&」は文字列連結演算子、「(」と「)」は単なる普通の括弧です)
例えば、以下のようなコードを考えます。
上記がすべて展開されると、内部的には以下のようなコードに変換されます。
逆に言えば、(${expr}の中身である)exprの部分には、 このように展開された後でもきちんと処理されるような式を書かなければならないことになります。
const str = @$__xxx__'AAA${expr}BBB'__xxx__
上記がすべて展開されると、内部的には以下のようなコードに変換されます。
const str = "AAA" & ( expr ) & "BBB"
逆に言えば、(${expr}の中身である)exprの部分には、 このように展開された後でもきちんと処理されるような式を書かなければならないことになります。
通常はあまり複雑でない式(例えば単独の識別子や、単なる配列要素へのアクセス、単なる構造体メンバへのアクセス、 単なる(値を返すような)関数呼び出し等)を指定することをお勧めします。 上記の「${delimiter}」が埋め込まれた例で考えますと、例えばもしここにdelimiterではなく、 宣言されていない別の変数名を指定した場合はコンパイルエラーとなりますが、 これは非常に単純な式ですので何が悪いのか瞬時にわかります。
しかしこの部分にあまりに複雑な式を記述すると、何か文法上の間違いを入れていざコンパイルエラーとなったときに、 ヒアドキュメント内部に埋め込まれている関係でエラーメッセージが混乱し、 どの部分がおかしいのかよくわからなくなるかもしれません(特にヒアドキュメントが複数行に渡る場合、 エラーメッセージで報告される行番号がヒアドキュメントの最後の行のものになることがあります)。
インデント下でのヒアドキュメント
インデントされている状況下でヒアドキュメントを使う場合を考えてみます。 目標はヒアドキュメントを使って次のように表示させることです。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
これを行うため、例えば以下のように記述したとしましょう。
{
/* この例ではすべての行の行頭にタブ文字が2つ入る */
const str = @__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
上の例では、ヒアドキュメントで指定した文字列リテラルをstrに代入し最後にそれを表示しています (最初の「{」と最後の「}」には特に意図はなく、説明の都合上インデントのレベルを深くしたいため強引につけただけです)。 しかし上記のように書くと以下のようにすべての行の行頭にタブ文字が2つ挿入された形で表示されてしまいます。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
しかも上記の字面からはわかりにくいですが、最後の行にも余計なタブ文字が一つ挿入されています。 おそらくこれは期待した表示ではないでしょう。
仮にこのようなタブ文字を挿入したくないならば、以下のように記述しなければなりません。
{
/* この例ではすべての行の行頭にタブ文字は入らないが、不恰好である */
const str = @__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__ /* 最後の余分なタブも表示されないようにここのインデントも消去 */
Rrk_print( str )
}
上記を実行した結果も以下に示します。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
このようにかなり頑張って記述すれば確かに余計なタブ文字の挿入を回避できるのですが、 ヒアドキュメントの所だけでインデントが崩れ、見た目の構造もわかりにくくなります。
そこでこの問題に対処するため、Rarakuのヒアドキュメントでは指定子のIndent設定部に「-」(ハイフン)を必要な数入れることで、 その個数分の行頭のタブ文字を削除するよう指定することができます。 これを使うと例えば以下のようにインデントを崩さず記述できます。
{
/* この例ではすべての行の行頭にタブ文字は入らないし、インデントも崩れない */
const str = @--__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
上の例では、「@__xxx__'」の替わりに「@--__xxx__'」と指定しています。 「-」を二つ指定していますので行頭にあるタブ文字二つ分は削除されます。 最後の行はタブ文字が一つしかありませんがこれも削除されます。 その結果、以下のように所望の結果を得ることができます。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
「-」記法(や後述する同類の記法)による修正はコンパイル時に静的に行われます。
一方、ヒアドキュメントを引数として整形用の関数を呼び出し、その内容を動的に修正する方法も考えられます。
実際、その方が柔軟ではあるのですが、当然ですがそのような関数を自分で用意しなければなりません。
また実行時での修正となるため、厳密には実行速度に差が生じることにはなりますが、これについては実質無視できるる差だとは思います。
ただ、この「-」を使った方法はシンプルではありますがまだ少し問題もあります。
この例で「-」を二つ指定する代わりに三つ指定してしまった場合どうなるでしょうか? つまり「@--__xxx__'」ではなく「@---__xxx__'」と書いてしまった場合です。 その場合、タブ文字が2個ある行については2個のタブ文字を、3個ある行については3個のタブ文字が削除されます。 その結果、以下のように「return 0;」の行だけおかしな表示になってしまいます。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
インデントのレベルが N の場合(タブ文字 N 個でインデントされている場合)、 「-」はその数に合わせて必ず N 個指定しなければなりません。 上の例ではインデントレベルが 2 であるにも関わらず「-」を3つ指定してしまったためにおかしな表示になったわけです。
この方法ではヒアドキュメントの周辺のインデントのレベルが変わった場合、 ヒアドキュメントの「-」の指定数も忘れずに修正しなおす必要があります。 これをなんとかできないでしょうか?
このような場合「-」の代わりに「|」を指定するとうまくいきます(今回は「|」は一つだけ指定してください)。 「|」一つを指定した場合、次のような手順で行頭にある余計なタブ文字を削除します。
- ヒアドキュメント内の文字列が複数行に渡る場合、それら全ての行について行頭にあるタブ文字数を調べ記憶しておきます。 ただしその行が空行である場合はスキップして記憶しません。 またその行がタブ文字だけからなる行である場合もこれをスキップして記憶しません。
- 記憶したタブ文字数の中で最小となる値 m を求めます。 ただし一つ前の手順ですべての行がスキップされるような特異な状況の場合、この m の値は 0 と定めます。
- すべての行における行頭のタブ文字 m 個分を削除します。 ただしその行が空行である場合はそのまま解釈します。 またその行がタブ文字だけからなる行で、そのタブ文字数が m に満たない場合は、 その行に存在するタブ文字をすべて削除します。
Rubyのヒアドキュメントの「~」指定をご存知の方は、
あれとだいたい同じような働きになると考えてよいと思います。
「|」を指定した例を以下に示します。
{
const str = @|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__ /* ヒアドキュメント内のタブ文字だけからなる行 */
Rrk_print( str )
}
上の例で、ヒアドキュメントの一番最後の行(終了パターン「'__xxx__」がある行)のタブ文字数は 1 です。 しかしこの行はタブ文字だけから構成される行であるため、 タブ文字数の最小値 m を計算する上では考慮されません。 従って今回導出される最小値 m は 2 となり、全体として行頭のタブ文字2つ分が削除されます。 これでちょうどうまい具合に余分なタブ文字が削除される形に落ち着きます。
実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
望み通りの結果です。
念のため、上の例のインデントレベルを変えてもう一度実行結果を確認してみましょう。 次のように1レベル深くしてみます。
{
{
const str = @|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__ /* ヒアドキュメント内のタブ文字だけからなる行 */
Rrk_print( str )
}
}
今回ヒアドキュメントの開始部分である「@|__xxx__'」には何も修正を加えていません。 単純に全体のインデントレベルだけを変えています。
実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
同じく望み通りの結果です。 「|」を指定すれば、たとえインデントのレベルが変わった場合でもうまくタブ文字を削除してくれることがわかります。
「|」で処理した結果を再インデント/再削除する
「|」の指定では全行の余計な行頭のタブだけを削って揃えてくれます。 ほとんどの場合これで十分でしょうが、さらにこの状態から指定した個数だけ全体をタブ文字で再インデントすることもできます。 そのためには「|」指定に加え、「+」を再インデントしたい個数分だけIndent設定部に指定します。 例えば次のようになります。
{
{
const str = @|++__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
}
上の例では「|」の後ろに「+」を二つ指定していますから、まず「|」指定による処理が行われた後、 その結果全体をタブで2レベルだけ再インデントします。
実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
もう少し正確に言えば「+」指定をした場合にインデントが加わるのは空行以外の行となります
(ただし「|」とは異なり、タブだけからなる行は空行とはみなされずインデントされます)。
また「|」と「+」を同時に指定した場合、まず先に「|」の処理が行われ、その後に「+」の処理が行われます。
従って「|」の処理によって空行化した行については、結果的に「+」の処理がスキップされることになります。
また「|」で処理した状態から、指定した個数だけ全体のタブ文字を再削除することもできます。 そのためには「|」指定に加え、「-」を再削除したい個数分だけIndent設定部に指定します。 例えば次のようになります。
{
{
const str = @|-__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
}
上の例では「|」の後ろに「-」を一つ指定していますから、まず「|」指定による処理が行われた後、 全体のタブ文字を1レベルだけ再削除します。
実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
「-」での再削除により、「return 0;」のある行にある凹んだ(深い)インデントまで削除されていることが確認できます。
「|」と「-」を同時に指定した場合、まず先に「|」の処理が行われ、その後に「-」の処理が行われます。
また削除可能なタブ文字数より多くの「-」を指定した場合、余分な「-」指定は単に無視されます。
インデントを完全に削除したい場合
ここまでは「|」を一つ指定する場合を見ましたが、 これを二つ指定した場合(つまり「||」などと指定した場合)、全てのインデントを問答無用で削除することができます。 これは最終的な出力にはインデントが一切あってはならない(しかしRaraku上ではインデントしたい)ような状況で便利です。
例えばVC(Microsoft製のCコンパイラ)付属のMakefileを例に挙げましょう。 このMakefileではRarakuで言うところの「#if」、「#else」、「#endif」のような書式があり、 それぞれ「!IF」、「!ELSE」、「!ENDIF」と記述します。 ただ困ったことにこれらはインデントができず、必ず行頭から書き始める必要があります。 つまり最終的には以下のようにしなければなりません。
!IF EXISTS(Makefile_version.mak)
!INCLUDE Makefile_version.mak
!ELSE
!IF EXISTS($(MKFB_MAKVER_PATH))
!INCLUDE $(MKFB_MAKVER_PATH)
!ENDIF
!ENDIF
今、これをRarakuのヒアドキュメントで記述することを考えます。 「|」を使えば、以下のように中身の文字列にインデントを置くこと自体は可能です。
const str = @|/"\r\n"__vc_makefile__'
!IF EXISTS(Makefile_version.mak)
!INCLUDE Makefile_version.mak
!ELSE
!IF EXISTS($(MKFB_MAKVER_PATH))
!INCLUDE $(MKFB_MAKVER_PATH)
!ENDIF
!ENDIF
'__vc_makefile__
上記では「|」に加え「/"\r\n"」の指定も併用し、生の改行コードを\r\nに変換しています。 ただしこのように併用する場合、必ずEmbedEscape指定部、Indent指定部、LineBreak指定部の順で指定しなければなりません。 「|」はIndent指定部の指定子、「/"\r\n"」はLineBreak指定部の指定子ですので、 上記のケースでは「|」を必ず先に書かなければなりません。
上記で一応インデントはできましたが、しかしこの場合、すべての行の開始位置は揃える必要があります。 さもなければインデントの凹んだ部分(一部のインデントの深い部分)を「|」で削除し切れないからです。
一方、「||」と指定すればインデントは(そのような凹んだ部分も含め)最終的に完全削除できるので、 さらに以下のような(階層構造を反映した)記述が可能になります。
const str = @||__vc_makefile__'
!IF EXISTS(Makefile_version.mak)
!INCLUDE Makefile_version.mak
!ELSE
!IF EXISTS($(MKFB_MAKVER_PATH))
!INCLUDE $(MKFB_MAKVER_PATH)
!ENDIF
!ENDIF
'__vc_makefile__
尚、今回の説明の趣旨とは関係ないですが、
実際にはVCのMakefileでは以下のように「!」と「IF」の間にスペースを入れることができますので、
ある程度構造をわかりやすく書くことも可能です。
!IF EXISTS(Makefile_version.mak)
! INCLUDE Makefile_version.mak
!ELSE
! IF EXISTS($(MKFB_MAKVER_PATH))
! INCLUDE $(MKFB_MAKVER_PATH)
! ENDIF
!ENDIF
「|」で削除されるインデント数を調整したい場合
「|」を一つ指定する方法と「+」指定子を組み合わせればほとんど全てのケースでうまく調整できるとは思うのですが、 この組み合わせでも調整できない非常に特殊なケースを挙げることもできます。 例えば以下をご覧下さい。
{
const str = @__xxx__'
aaa
'__xxx__
}
字面では分かりにくいのですが、上記は一行目のaaaの前に三つのタブ文字があり、 次の行(二行目)では二つのタブ文字があります(マウスで選択状態にして反転表示にすると少しだけ分かりやすいかもしれません)。 そして(あまりない要求だとは思いますが)外側のインデントは消去し、 一行目のaaaの前には二つのタブ文字、 次の行(二行目)では一つのタブ文字だけを忠実に残したいものとします。
aaaの前の二つのタブを残したいだけなら話は簡単で、「|++」と指定すればOKです。 しかし問題は次の行(二行目)にある一つのタブ文字です。
(一つの)「|」の削除アルゴリズムではタブ文字だけからなる行は削除量の計算式にカウントしないため、 このときの削除量は2と計算されます。 そしてこの場合、二行目にあるタブ文字の個数は(aaaのインデントレベルより少ないため)跡形も残らず削除されます。 この後「++」記号でタブ文字を復活させると、(一行目はそれで問題ありませんが) 二行目は既に空行と化しており、タブ文字は復活しません(「+」記号は空行には作用しません)。
繰り返しになりますが、今、二行目のタブ文字を本当に一つだけ残したいものとします。
「元々タブ文字だけで構成されていた行の、そのタブ文字の一部だけを忠実に残したい」
そういった要求のある実用的な例をあまり想定することができませんが、
例えば何らかの解説記事で「変なインデントが残る状態そのもの」を記述したい場合
(実は今私が書いているこの記事自体がまさにそうですが)、
このような変な記述ができないと若干指定が面倒というのはあるかもしれません。
「|」を使わず「-」だけを使えば一応これを実現できますが、この場合既に説明したように 外側のインデントレベルが変わると「-」の数を修正しなければならなくなるという別の問題が発生します。
そこでこれに対処する最後の手段として、「|」を三つ指定する方法があります。 これはちょうど終了パターンのインデントレベルの量だけ全体のインデントを削除するというものです。
この挙動はSwiftやC#での方式とほぼ同じだと思います。
もう少し正確に言えば「|||」を指定した場合、
まずヒアドキュメント内部における最後の改行の位置をスキャンします。
最後の改行が見つかった場合、その位置からヒアドキュメントの終了までの間がすべてタブ文字であるなら、
その場合に限り、それらのタブ文字をすべてカウントし、その個数を最終的な削除数とします。
一方、改行が一つも見つからなかった場合や、最後の改行以降にタブ文字以外のものが含まれていた場合は削除数を0とします(つまり何も行いません)。
例えば以下をご覧下さい。
{
const str = @|||__xxx__'
aaa
'__xxx__
}
上記では「|||」を指定しておりますので、まず終了パターンである「'__xxx__」のインデントレベルを考えます。 上記では「'__xxx__」の前に一つのタブ文字がありますので、 全体としても1レベル分のタブ文字が削除されます。 実行結果は以下の通りです。
aaa
といっても実行結果も字面からは分かりづらいですが、 一行目のaaaの前には二つのタブ文字、 次の行(二行目)では一つのタブ文字だけが残っており、所望の結果となっています。
この方式は柔軟性はあるのですが、場合によっては終了パターンのインデントレベルを強引に変えなければなりません。 例えば今まで扱ってきた以下の例を再び考えます。
{
const str = @|__C_source_code__'
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
}
実行結果を以下のようにさせたいとしましょう。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
仮にこれを「|||」単独で書くとすると、終了パターンの位置を少々後ろへずらす必要があります。
{
const str = @|||__C_source_code__'
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
}
上記は別にそこまで見難いというほどのことはないと思いますが、 もしもこの位置の変更を嫌うならやはり単に「|」とするか、 あるいは「|||」と「-」記号を併用する方法もあります。 例えば以下の通りです。
{
const str = @|||-__C_source_code__'
int main( int argc, char** argv )
{
return 0;
}
'__C_source_code__
Rrk_print( s )
}
上記はまず「|||」の処理によりインデントが1レベル削除され、次に「-」の処理によりインデントが1レベル削除されますので、 望みの結果になります。
ヒアドキュメントの終了行パターンにある余計なインデントだけを削除する
Rarakuのヒアドキュメントでは、Indent設定部に「!」を指定することで、終了パターンの直前にある余計なインデントだけを削除できます。 例えば以下をご覧下さい。
{
{
const str = @!__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
}
上記の例では、終了パターン「'__xxx__」のある行の行頭に2つのタブ文字があります。 開始パターンの部分で「!」を指定することによって、これらのタブ文字をすべて削除できます。 実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
尚、「!」が作用するのは、終了パターンのある行だけであり、その他の行には何の作用もありません。 上記の例では「!」以外は何も指定しておりませんので、その他の行のインデントはそのままの状態で残っています。
もう少し正確に言えば「!」を指定した場合、
まずヒアドキュメント内部における最後の改行の位置をスキャンします。
最後の改行が見つかった場合、その位置からヒアドキュメントの終了までの間がすべてタブ文字であるなら、
その場合に限り、それらのタブ文字をすべて削除します。
一方、改行が一つも見つからなかった場合や、最後の改行以降にタブ文字以外のものが含まれていた場合は何もしません。
「!」指定子の挙動はわかりましたが、これはどのような場合に使うのでしょうか?
「|」の場合、それ一つで完結するか「+」や「-」と組み合わせて使うことを想定しています。 このとき元々のインデントレベルを一旦0にしてそこから必要に応じてインデントレベルを増減する処理になります。 つまりこれは、基準を0の位置とした絶対的な処理です。
一方、「!」の場合もやはり、それ一つで完結するか「+」や「-」と組み合わせて使うことを想定しています。 このとき元々のインデントレベルから増減し、終了パターンのある行のインデントについては特別に「!」で 完全削除するといった処理になります。 つまりこれは、基準を元の位置とした相対的な処理です。
どちらかといえばレアなケースになるとは思いますが、字面どおりのインデントのレベルをある程度反映して解釈して欲しい状況では、 このような相対的な指定の方がインデントレベルの変更に伴う「調整のし直し」を防ぐことができます。
例えば以下の例をご覧下さい。 これはJavascriptを含むHTMLをRarakuBIRDと呼ばれる仕組みを用いて自動生成するというやや特殊な例です。
ただしこの例はrarakuコマンド単独では動作しません。
このチュートリアルはRarakuBIRDの説明ではないため、その稼動方法についての詳しい説明は省略します。
<script type='text/javascript'>
function onFinally()
{
#[<rrk>
Rrk_output( @|++__here__'
var unit_cls_name = 'rts';
var reload_msg = 'OK';
'__here__ )
</rrk>]#
return 0;
}
</script>
上記の「#[<rrk>」と「</rrk>]#」の間に挟まれた部分はRarakuのコードであり、Rrk_outputはRarakuの関数です。 ここではRrk_outputで指定した文字列が、このJavascriptのコードのこの位置に埋め込まれる仕組みとなっています。 つまり上記の例では、最終的に以下のようなJavascriptのコードを生成したいものとします。
<script type='text/javascript'>
function onFinally()
{
var unit_cls_name = 'rts';
var reload_msg = 'OK';
return 0;
}
</script>
元々の(ヒアドキュメントで囲まれた)「var unit_cls_name = 'rts';」と「var reload_msg = 'OK';」の行において、 そのインデントレベルは3でした(3つのタブ文字でインデントされていました)。 これを最終的には上記の結果のように、インデントレベルを2(2つのタブ文字でインデント)にしたいわけです。
そのため、元々のヒアドキュメントの開始部分において、「@|++__here__'」と指定しています。 これはまず「|」により元々のインデントレベルを一旦0にし、そこから改めて「+」によりインデントレベルを2に調整するというものです。
しかしこれは結局、外側のJavascriptのインデントが変わるたびに「+」の指定も調整しなおさなければならないことを意味します。 例えば以下のようにインデントレベルが変わった場合、「++」ではなく「+++」としなければなりません (「#[<rrk>」については、埋め込み記号の開始を示すという性質上、この状況では必ず行頭に置く必要があります(さもなければそれより前の余計なタブ文字が残ってしまいます)が、 とりあえずこれはやむを得ないものとしましょう)。
<head>
<script type='text/javascript'>
function onFinally()
{
#[<rrk>
/* インデントレベルが変わったので「++」から「+++」に変更した */
Rrk_output( @|+++__here__'
var unit_cls_name = 'rts';
var reload_msg = 'OK';
'__here__ )
</rrk>]#
return 0;
}
</script>
</head>
このような場合、元々のインデントレベルを一旦0にしてそこからインデントレベルを増やすよりも、 元々のインデントレベルから減算した方が「調整のし直し」を防ぐことができます。 つまり「|」を使わず「-」だけで調整します。
この例では、要するに「Rrk_output(」と「)」の間にある余計な一つ分のインデントをカットしたいだけですから、 「|+++」と指定するよりも、「-」と指定する方が望ましいと言えます。 ただし単に「@-__here__'」と指定したのでは問題があります。 この例の場合、ヒアドキュメントの終了パターンのある行(つまり「'__here__」がある行ですが)において、 余計なタブ文字が3つほどありますが、このタブ文字は通常はすべてカットしたいでしょう。 この状況では「-」は一つだけしか指定できませんが、それだとまだ2つほど残ってしまいます。 それを処理するために「!」が必要になります。
結局今回の例の場合、「@!-__here__'」と指定すればよいことになります。
タブ文字ではなくスペースでインデントしたい場合
ここまでの説明ではインデント文字としてすべてタブ文字を前提として説明してきました。 しかしタブ文字ではなくて(半角)スペースでインデントしたい派の方もおられると思います。 その場合、Indent設定部に「~」と一桁の数字を指定します。
ただしこの「~」指定子は、それを指定したヒアドキュメント内に限って有効になります。
例えば以下をご覧下さい。
{
{
const str = @|~2__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
Rrk_print( str )
}
}
上記の例は、一つのインデントとして(半角)スペース2文字分を当てています。 この場合、ヒアドキュメントの開始パターンに「~2」を加えて「@|~2__xxx__'」とすれば、 今までの説明と全く同様に処理させることができます(今までの説明における「タブ文字」を「スペース2文字」に読み替えるだけです)。
あるいは一つのインデント単位としてスペース4文字分を当てている場合であればこれを「~4」とすればうまく処理されるでしょう。 尚、単に「~」と指定することもでき、その場合は「~1」(半角スペース1文字分を一つのインデント単位として処理するモード)を指定したのと同じ意味になります。
繰り返しになりますが「~」の後ろの数字は必ず一桁でなければならないことに注意してください。
「~12」などのように二桁の数を指定することはできません。
「~0」と指定することもでき、その場合はまた特別で、一つのインデント単位が一個のタブ文字であるとして処理します。 ただしデフォルトでは、「~」を指定しない場合は、暗黙のうちに「~0」が指定されたものとみなされます。 そのため、特に何もしなければ最初からタブ文字とみなして処理されるというわけです。
すべてのヒアドキュメントで毎回「~」と「スペース文字数」を指定するのが煩わしい場合、 pragma文でindent_typeにスペース文字数を指定します。 例えば、インデントの一単位として(半角)スペース4文字分を当てたい場合、以下のようにindent_typeに4を指定します。
pragma indent_type:4
このpragma文が実行された後は、そのファイルにおいてデフォルトが「~4」(半角スペース4文字分を一つのインデント単位として処理するモード)となります。 つまり特に「~」を指定しなくとも、暗黙のうちに「~4」が指定されたものとみなされます。
pragma indent_typeに指定する数字は必ず0から9までの一桁の数字でなければなりません。
1から9までを指定すると半角スペースをその数字分並べたものを指定したことになり、0を指定するとタブ文字一個を指定したことになります。
例えば、上記の状態からさらに「pragma indent_type:0」を実行するとデフォルトが再び「~0」
(タブ文字を一つのインデントとして処理するモード)に戻ります。
一方、この状態でも各ヒアドキュメントで明示的に「~」を指定すれば、その明示的な指定の方が優先されます。 例えばこの状態で「~0」と指定すると(デフォルトは半角スペース4文字に変更されてはいますが)、 そのヒアドキュメントに限り、一つのインデント単位がタブ文字であるとして処理されます。
タブ文字をインデント文字として解釈している状態(デフォルト)ではスペースはその他の文字と同じように扱われますが、
逆に「~」指定子によりスペースをインデント文字として解釈している状態ではタブ文字がその他の文字と同じように扱われます。
これは、これまでに説明してきた指定子が正常に作用するには、基本的に「すべての行がタブ文字でインデントされている」か 「すべての行が(半角)スペースでインデントされている」かのいずれかでなければならないということです。 それがタブ文字とスペースを混ぜたような変なインデントである場合、 (例外的にうまくいく場合もあるでしょうが)基本的にこれまでの説明のような効果は得られないでしょう。
「-」指定子や「+」指定子の直後にも「~」と同様に一桁の数字を書くことができ、
このように記述するとその数字の回数「-」や「+」を指定したのと同じ効果になります。
例えば「----」と書く替わりに「-4」と書くこともできるということです。
これを使えば、「-」を12個書く替わりに例えば「-4-4-4」と書くことができます。
この方が(「------------」と比べれば)「-」の個数も数えやすいと思います
(こんなにインデントレベルを削除することはまずないとは思いますが)。
ただし、「-」指定子の直後に書けるのはあくまで一桁の数字だけですから、「-12」などのように二桁の数を指定することはできません。
これは、これまでに説明してきた指定子が正常に作用するには、基本的に「すべての行がタブ文字でインデントされている」か 「すべての行が(半角)スペースでインデントされている」かのいずれかでなければならないということです。 それがタブ文字とスペースを混ぜたような変なインデントである場合、 (例外的にうまくいく場合もあるでしょうが)基本的にこれまでの説明のような効果は得られないでしょう。
この指定はスペース一文字単位でもっと細かく削除や追加を行いたいという場合に便利かもしれません。 例えば、そのRarakuファイル全体としては半角スペース4文字分を一つのインデント単位として記述している場合に、 ヒアドキュメントでは「~4」ではなく敢えて「~」(つまり「~1」と同じことですが)を指定し、 スペース1文字ずつ細かく制御したいといったような場合です。 例えば以下の通りです。
{
{
/*..{...{...{...{*/
const str = @~-4-4-2__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
/*..{...{...{...{*/
Rrk_print( str )
}
}
「/*..{...{...{...{*/」とある妙なコメントは、単に何文字分半角スペースが入ってるかを見やすくするモノサシとして使っていますので 直接関係はありません。 上記では「~4」ではなく敢えて「~」(「~1」と同じ)と指定していますので、このヒアドキュメントにおいてはスペース一文字がインデントの単位とみなされて処理されます。 そのためまず「-4-4」により、スペース8文字分が削除され、その後に「-2」によりスペース2文字分が削除されます。 実行結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
「return 0;」の行のインデントについては2文字分削除され、残り2文字が残っているのが確認できると思います。 内部的には「~」に指定された数字と「-」に指定された数字の掛け算として最終的なインデント削除量が計算されます。 例えば「~4」と書いた所にさらに「-2-2-2」と書いたとします。この記述は内部的には「-2」に「~」による指定数「4」が乗算されて 結果的に(「~」あるいは「~1」と書いた所に)「-8-8-8」と書いたのと同じ処理に変換されるということです。 「+」についても同様です。
これについて理解を確認するため、全く同じ処理を行う二つの例を見ましょう。 まず以下をご覧下さい。
{
{
/*..{...{...{...{*/
const str = @~4---__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
/*..{...{...{...{*/
Rrk_print( str )
}
}
上記は以下のように「~4」を「~」に、「-」を「-4」に置き換えて記述することもできます。
{
{
/*..{...{...{...{*/
const str = @~-4-4-4__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return 0;
}
'__xxx__
/*..{...{...{...{*/
Rrk_print( str )
}
}
インデントに関する指定子のまとめ
インデントに関する指定子(Indent設定部)について、これまでの内容を模式的にまとめると次のようになります。
| 指定子 | 効果 | 優先順位 |
|---|---|---|
| ^ | ヒアドキュメントの最初の文字が生の改行であった場合それをカットしない | 指定された場合、最初に設定される |
| ~ | インデント文字(インデントタイプ)の変更 | 指定された場合、最初に設定される |
| | | 全体のインデントをコードの凹凸は残す形で可能な限り減らす、あるいは全部削除 | 指定された場合、「~」の次に実行される。 |
| - | 全体のインデントを一つ減らす | 指定された場合、「|」の次に実行される。 |
| + | 全体のインデントを一つ増やす | 指定された場合、「|」の次に実行される。 |
| ! | 最後の改行以降が全てインデント文字な場合それをすべて削除 | 指定された場合、「+」や「-」の次に実行される。 |
これらの指定子はIndent設定部内をはみ出さない限りはどういう順番で指定しても構いませんが、その実行は優先順位にあるとおりの順番で行われます。 またそれぞれの指定子の間にスペース等を入れることはできません。 それぞれの詳しい仕様は以下の通りです。
- ^指定子
- ~指定子
- |指定子
- -指定子
- +指定子
- !指定子
一つ指定すれば十分であり、二個目以降は意味はない。 Rarakuのヒアドキュメントの中身が複数行に渡る場合、 デフォルトでは一番最初の文字が生の改行であった場合、それを削除する。 この指定子を付けた場合、その削除を行わない。
一つ指定すれば十分であり、二個目以降は意味はない。 通常、後ろに0から9までの一桁の数字を指定する(ただし指定しなかった場合は「~1」と同じとみなされる)。 「~0」と指定した場合、タブ文字を一つのインデント単位として処理する。 「~N」と指定した場合、半角スペースN文字を一つのインデント単位として処理する(ただしNには1から9までの数字が入る)。
デフォルトでは特にこの指定子がなくても暗黙のうちに「~0」が指定されたのと同じとみなされる。 そのため、Rarakuのヒアドキュメントでは、通常インデントとして使用する文字をタブ文字とみなして処理を行う。 このデフォルトを変更したい場合、「pragma indent_type:N」と指定する(ただしNには0から9までの数字が入る)。
タブ文字を一つのインデント単位として処理している場合、 半角スペースはインデントとは関係のない通常の文字とみなされる。 一方、半角スペースN文字を一つのインデント単位として処理している場合、 逆にタブ文字がインデントとは関係のない通常の文字とみなされる。
従って、仮にスペースとタブ文字の両方を混合してインデントしているような変な状況にはそもそも対応できない。 この効果はこの指定子を指定したヒアドキュメントの中だけで有効である。 「pragma indent_type:N」によって全体のデフォルトを変更した場合でも、 この指定子を明示的に指定した場合は、その明示的な指定が優先される。
最大三つまで指定でき、何個指定したかで意味が異なる(四つ以上は三つ指定したのと同じである)。
これを一つだけ指定した場合、まずヒアドキュメント全体の行をスキャンし、その中で最もインデントのレベルが小さいものを探し出し、 そのインデントのレベルをmとする(ただし空行やインデント文字だけからなる行は、このスキャンの対象外である)。 次に、全体の行のインデント文字をm個分削除する (ただし空行だけからなる行については何もしない。インデント文字だけからなる行については他の行と同様にm個分のインデントが削除される)。 大抵の場合、「|」を一つだけ指定すれば所望の結果になるはずである。
これを二つ指定した場合、行頭のインデントを完全に削除する。 つまり上記に加え、いわゆるインデントの凹んだ行に至るまですべてのインデントを削除する。 これは(プログラマの見やすさのために)Rarakuコード上は中身の文字列値をインデントしたいが、 最終的な出力はそのようなインデントがつかないものとしたいような場合(例えばHTMLの記述など)で (特にLineBreak設定部の記法と組み合わせると)便利である。
これを三つ指定した場合、終了パターンのインデントレベルと同じ個数分、全体のインデントを削除する (いわゆるSwiftやC#で採用されている方式とほぼ同じである)。 より正確には、まずヒアドキュメントの最後の生の改行を探しだし、 その位置からヒアドキュメントの終わりまでの間の文字がすべてインデント文字であった場合、 それらのインデント文字の個数をカウントする(その間に一つでもインデント文字でないものがあった場合は0とする)。 最終的に算出されたそのカウント数だけ全体の行のインデントを削除する。
「-」や「+」指定子と一緒に使った場合、まず「|」指定子の処理が行われた後に その状態から増減がなされる。 「!」指定子と一緒に使った場合、まず「|」指定子の処理が行われた後に「!」指定子の処理が行われるが、 通常、両者は論理的には互いに干渉しない。「|」指定子の処理の結果がどうであれ、 終了パターンの直前にある余計なインデントは「!」指定子の処理によって確実に削除される結果に違いはないからである。
複数指定することができ、指定した個数分のインデント文字を削除する。 これはヒアドキュメント内の(空行を除く)全ての行について行われる。 インデントの文字数が指定個数に満たない行については、それらのインデント文字が全部削除される形になる。
例えば「---」と指定すれば3個分のインデント文字を削除する。 -指定子の直後に一桁の数字を書く記法があり、これを使うとその数字の回数「-」を指定したのと同じ効果になる。 例えば「---」と書く替わりに「-3」と書くこともできる。 ただし、-指定子の直後に書けるのはあくまで一桁の数字だけであり、「-12」など二桁以上の数を指定することはできない。 このような場合、「-」を直接12個書いてもよいが、例えば「-4-4-4」と書くこともできる。 また「-0」は何も指定していないのと同じであり、「-1」は単に「-」と指定したのと同じである。
この指定子はまず「|」で処理を行った後、 最後の微調整として「-」で全体のインデントを必要なだけ削除するというのが典型的な使い方になろう。 あるいは「!」と併用し、全体のインデントを元のレベルから相対的に削除するような使い方もできる。
複数指定することができ、指定した個数分のインデント文字を追加する。 これはヒアドキュメント内の(空行を除く)全ての行について行われる。
例えば「+++」と指定すれば3個分のインデント文字を追加する。 +指定子の直後に一桁の数字を書く記法があり、これを使うとその数字の回数「+」を指定したのと同じ効果になる。 例えば「+++」と書く替わりに「+3」と書くこともできる。 ただし、+指定子の直後に書けるのはあくまで一桁の数字だけであり、「+12」など二桁以上の数を指定することはできない。 このような場合、「+」を直接12個書いてもよいが、例えば「+4+4+4」と書くこともできる。 また「+0」は何も指定していないのと同じであり、「+1」は単に「+」と指定したのと同じである。
この指定子はまず「|」で処理を行った後、 最後の微調整として「-」で全体のインデントを必要なだけ追加するというのが典型的な使い方になろう。 あるいは「!」と併用し、全体のインデントを元のレベルから相対的に追加するような使い方もできる。
尚、あまり意味はないが「-」指定子と一緒に使った場合、その結果は互いに打ち消しあう。 つまりどちらの絶対値が大きいかによって最終的にどちらとして処理されるかが決まる。 例えば「-2+5」と指定した場合、これは結局「+3」と指定したのと同じである。
一つだけ指定する用法と二つ指定する用法がある(三つ以上は二つ指定したのと同じである)。
この指定子は、ひと言で言えば、ヒアドキュメントの終了パターンの直前にあるインデントをすべて削除したい場合に使う。 より正確には以下の通りである。
これを一つだけ指定した場合、まずヒアドキュメントの最後の生の改行を探しだし、 その位置からヒアドキュメントの終わりまでの間の文字がすべてインデント文字であった場合、 それらのインデント文字を削除する(その間に一つでもインデント文字でないものがあった場合は何もしない)。 ヒアドキュメント内に改行が一つもない場合は何もしない。
これを二つ指定した場合、まず上記の処理をした上で、 一番最後の文字として残っているはずの(ファイル上の)改行も削除する。 これは「最後の改行(ただし一番最後の文字ではない)を削除する」のとは少し意味が異なることに注意しよう。 最後の改行からヒアドキュメントの終わりまでの間に一つでもインデント文字でないものがあった場合は何もしない。
「+」指定子と一緒に使った場合、(「!」指定子の方が後に実行されるため)「!」指定子の効果が最終的に反映される。 つまり終了パターンの直前のインデントが復活するようなことは起きない。
ヒアドキュメントの改行を指定した文字列で置換する
Rarakuのヒアドキュメントでは、文字列リテラル内部の生の改行コードを 指定した文字列で置換することができます。 そのためにはLineBreak設定部において、「/"..."」を指定します。 ここで「"」により囲まれる「...」の部分には置換後の文字列を指定します。 例えば以下の通りです。
const str = @/"<br>"__auto_br__'
aaa
bbb
ccc
'__auto_br__
Rrk_print( str )
上記では、LineBreak設定部において「/"<br>"」となっています。 この場合、ヒアドキュメント内のすべての生の改行コードを「<br>」に置き換えます。 実行結果は以下のようになります。
aaa<br>bbb<br>ccc<br>
実行結果では改行が消えたため、全ての行が「<br>」によって繋がって一行となっています。 「<br>」の後に改行するようにしたければ、LineBreak設定部において「/"<br>\n"」と指定すればよいでしょう。 例えば以下の通りです。
const str = @/"<br>\n"__auto_br__'
aaa
bbb
ccc
'__auto_br__
Rrk_print( str )
実行結果は以下のようになります。
aaa<br>
bbb<br>
ccc<br>
既に述べた通り、ヒアドキュメント内にある生の改行コードは強制的に「\n」に変換されます。 つまり上記は、「"」を使って以下のように明示的に「\n」を指定して書いたのと同じです。
const str = (
"aaa<br>\n"
"bbb<br>\n"
"ccc<br>\n"
)
Rrk_print( str )
念のため、もっと極端な例を挙げましょう。 以下は生の改行コードを、「\n」(Unix系の改行)に変換します。
const str = @/"\n"'
aaa
bbb
ccc
'
Rrk_print( str )
しかしながら、何度も繰り返しになりますが、ヒアドキュメント内にある生の改行コードは最初から「\n」に変換されているため、 上記の変換は「\n」から「\n」への変換となり、結局何もしていないのと同じになります。
一方、以下のように生の改行コードを、「\r\n」(Windows系やHTTPヘッダでの改行)に変換するのは有用な方法です。
const str = @/"\r\n"'
aaa
bbb
ccc
'
Rrk_print( str )
この実行結果は以下のようになりますが、「\n」と「\r\n」のどちらであっても通常、コマンドプロンプトやターミナル、テキストエディタ上では普通に改行されて表示されますから、 見かけ上は何も変わったように見えないかもしれません。
aaa
bbb
ccc
ヒアドキュメントの中身における生の改行を(\nと\r\nのどちらにも強制変換せず)本当にありのままに含めたいといった要求が状況によってはあるかもしれません。 あまり推奨はしませんが、LineBreak設定部において「/<raw>」と書くことによって一応これは可能です。 例えば以下の通りです。
const str = @/<raw>'
aaa
bbb
ccc
'
Rrk_print( str )
上記のように書いた場合、中身に含まれる生の改行が\nと\r\nのどちらであるかは、 そのファイルの改行コードがどうなっているかに完全に依存します。
この記法はデメリットの方が多いと思われます。
とはいえ、何か実害が出るといったことも実は案外ないのかもしれません。
例えばHTMLなど改行コードを柔軟に処理するようなシステムは案外あり、
それへ向けた出力を作るならば改行コードが少々乱れていても実質的な問題はないと考えられるからです。
一方、これが有用な例もあまり思いつきません。 例えばRarakuのコード自体を自動生成するようなシステムで、しかもヒアドキュメントの内部にも外部から別の文字列が埋め込まれ、 しかもその文字列の生の改行コードが\r\nと\nのどちらともなり得るような処理の場合に必要になるのかも(?)しれません。 いずれにしても\r\nと\nが混じったような出力を嫌うならば、「/<raw>」という指定子は避けた方が無難だと思います。
一方、これが有用な例もあまり思いつきません。 例えばRarakuのコード自体を自動生成するようなシステムで、しかもヒアドキュメントの内部にも外部から別の文字列が埋め込まれ、 しかもその文字列の生の改行コードが\r\nと\nのどちらともなり得るような処理の場合に必要になるのかも(?)しれません。 いずれにしても\r\nと\nが混じったような出力を嫌うならば、「/<raw>」という指定子は避けた方が無難だと思います。
様々な指定子やその他の要素の併用
ここまで多くの指定子が登場しましたが、これで一応すべてを網羅したことになります。 ここからはこれらを併用する場合の注意点について見て行きましょう。 以下をご覧下さい。
const val = 10
const str = @$|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return ${val};
}
'__xxx__
Rrk_print( str )
上記の例では、ヒアドキュメントの開始パターンは「@$|__xxx__'」となっています(「$」と「|」を指定しています)。 「$」はEmbedEscape設定部、「|」はIndent設定部ですので、「$」は「|」より先に書かなければならないことに注意してください。 「|$」のように逆にするとコンパイルエラーとなります。
文字列リテラル内の「return ${val};」の部分は、(ヒアドキュメントの外にある)変数valの値(今回は10)で置き換えられます。 すなわちこの部分は結果的に「return 10;」と展開されます。
${...}の内部にコメントアウトの記号(「/*」「*/」「//」「#if0」「#end」など)を記述する場合、いくつかの注意点があります。
上記の例では、「${...}」内に(コメントアウト内ではありますが)ヒアドキュメントの終了パターン「'__xxx__」と全く同じ文字列が現れています。 このような場合、「${...}」の指定の方が終了パターンよりも優先されます。 つまり「${...}」内は親となるヒアドキュメントとはもはや独立して解析され、 その中に現れた「'__xxx__」という文字列も、親となるヒアドキュメントの終了パターンとはみなされないということです。 この場合、BBBの直後にある「'__xxx__」が全体としての終了パターンとなります。
多分大抵の場合、これは期待通りの結果でしょう。 実行結果は以下のようになります。
尚、上記では「@$」が指定され、変数埋め込みが有効であるためにこのような挙動になりますが、 そうではない場合は、勿論すべてが単純に文字列ですから「/*」の直後にある「'__xxx__」がこのヒアドキュメントの終了パターンとなります (その後に続くトークンがもはや滅茶苦茶なのでコンパイルエラーとなるでしょう)。
ヒアドキュメントの外側に「/*」と「*/」があり、
さらにヒアドキュメントの${...}内にも「/*」と「*/」がある状況
この場合は通常のコメントアウトのネストの規則と同様に考えることができます。 例えば以下をご覧下さい(ただしこれはコンパイルエラーとなります)。
上記では、まずヒアドキュメントの外側に「/*」がありますが、 これによってコメントアウトされる範囲は、この場合${...}内にある「*/」の部分までです (Rrk_print関数の呼び出しの後にある「*/」ではありません)。 外側から全体をコメントアウトしたい場合は「/*」と「*/」ではなく「#if 0」と「#endif」で囲いましょう。
「#if」「#else」「#endif」を${...}の内外を跨ぐような変な位置で使った場合
あまりない状況とは思いますが、一応想定できます。 例えば以下をご覧下さい。
「あまりない状況」どころかもはや奇妙な状況ですが、ここでは強引に想定しましょう。 一応、「#if 1」ならば、ヒアドキュメントの前半の値を「AAA」に、 「#if 0」ならば、ヒアドキュメントの前半の値を「aaa」にする意図を持って書いたものとします。
この場合、ヒアドキュメントの開始パターンは解釈されますので、まずはヒアドキュメント全体の範囲を確定する処理を行いますが、 ただし「@$」と指定されていますので同時に変数展開も行われます。
「#if 1」の場合、「AAA」の直後の「${」より子となる字句解析が行われます。 このとき親の持つ「#if 1」という情報を子が引継ぎます。 従って上の例では、子の字句解析の開始時に「#else」というトークンがいきなり現れても、 期待通りの処理を行うことができます。 この場合の実行結果は以下のようになります。
上の例で、もしも「#if 0」であった場合も同様にうまくいきます。 この場合は、まず「#else」まで普通にスキップし、「aaa」の直後の「${」より子となる字句解析が行われます。 このとき親の持つ「#if 0」「#else」という情報を子が引継ぎます。 従って上の例では、子の字句解析の開始時に「#endif」というトークンがいきなり現れても、 期待通りの処理を行うことができます。 この場合の実行結果は以下のようになります。
もっとも、(今回は挙動の説明のため敢えてこのような書き方をしましたが)普通はこのような書き方をすべきではありません。 するにしても以下のようにヒアドキュメントの外と「${...}」の内部を跨がないように工夫した方がまだ幾分は見やすいかもしれません。
「//」で「}」がコメントアウトされるような場合
あまりない状況とは思いますが、一応想定できます。 例えば以下をご覧下さい。
上記の例では、ヒアドキュメントの${...}内に「//」で一行コメントアウトしています。 通常、「//」で一行コメントアウトをするとその効果は行末まで続き、 ファイルの最終行で「//」を使った場合、仮にそのファイルの文末に改行がなくともそこでコメントアウトの範囲は終わります。 上記は「${」から辿って一番最初に現れる「}」(「;」の直前にある「}」)が、見方によっては ${...} の終わりの記号「}」と見えなくもありません。 しかし実際には、これは「//」によるコメントアウト内の単なるコメント文字列の一部です。 つまりRarakuの字句解析器はこの「}」や行末の「;」をコメントとしてスルーし、 次の行にある「}」に至ってそれを${...}の終わりの記号「}」とみなすことになります。 そのため、実行結果は以下のように途中で途切れた感じになります。
- ${...}の内部のコメントの中にヒアドキュメントの終了パターンを記述した場合
あまりない状況とは思いますが、一応想定できます。 例えば以下をご覧下さい。
const val = 10
const str = @$|__xxx__'
AAA
${
val
/* '__xxx__ */
}
BBB
'__xxx__
Rrk_print( str )
上記の例では、「${...}」内に(コメントアウト内ではありますが)ヒアドキュメントの終了パターン「'__xxx__」と全く同じ文字列が現れています。 このような場合、「${...}」の指定の方が終了パターンよりも優先されます。 つまり「${...}」内は親となるヒアドキュメントとはもはや独立して解析され、 その中に現れた「'__xxx__」という文字列も、親となるヒアドキュメントの終了パターンとはみなされないということです。 この場合、BBBの直後にある「'__xxx__」が全体としての終了パターンとなります。
多分大抵の場合、これは期待通りの結果でしょう。 実行結果は以下のようになります。
AAA
10
BBB
尚、上記では「@$」が指定され、変数埋め込みが有効であるためにこのような挙動になりますが、 そうではない場合は、勿論すべてが単純に文字列ですから「/*」の直後にある「'__xxx__」がこのヒアドキュメントの終了パターンとなります (その後に続くトークンがもはや滅茶苦茶なのでコンパイルエラーとなるでしょう)。
Rarakuのヒアドキュメントでは、基本的にはまずヒアドキュメント全体の範囲を終了パターンにより確定してから、
指定子の設定部の処理に入ります。ただし変数埋め込みが有効な場合は、その範囲の判定処理中に
変数の展開処理も同時に行われます。
この変数の展開処理においては、終了記号は「}」となりますが、その正確な位置は実際に「{...}」内を字句解析してみなければわかりません
(「{」と「}」が内部でネストされているかもしれませんし、文字列リテラルやコメントアウト内にそれらが含まれている可能性すらあります)。
そのため ${...} 内部では、ヒアドキュメントとしての処理が一旦中断され、いわば子となる字句解析が開始されますが、 そのときに親となるヒアドキュメントに関する情報は引き継がず、(それが別途スタックに保存された上で)クリアされます。 例えば上記のように「/* '__xxx__ */」が「${...}」内部で現れても、子の字句解析においてはヒアドキュメントの情報は存在しませんから、 そこでは単なるコメント内の文字列に過ぎずスルーされるわけです。 一つの変数展開が終わると子の字句解析も終了し、再び親の字句解析に制御が戻りますが、 このときに親となるヒアドキュメントに関する情報も(スタックから)復元されます。 あとはこの繰り返しとなります。
そのため ${...} 内部では、ヒアドキュメントとしての処理が一旦中断され、いわば子となる字句解析が開始されますが、 そのときに親となるヒアドキュメントに関する情報は引き継がず、(それが別途スタックに保存された上で)クリアされます。 例えば上記のように「/* '__xxx__ */」が「${...}」内部で現れても、子の字句解析においてはヒアドキュメントの情報は存在しませんから、 そこでは単なるコメント内の文字列に過ぎずスルーされるわけです。 一つの変数展開が終わると子の字句解析も終了し、再び親の字句解析に制御が戻りますが、 このときに親となるヒアドキュメントに関する情報も(スタックから)復元されます。 あとはこの繰り返しとなります。
この場合は通常のコメントアウトのネストの規則と同様に考えることができます。 例えば以下をご覧下さい(ただしこれはコンパイルエラーとなります)。
const val = 10
/*
const str = @$|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return ${
/* This is a value. */
val
};
}
'__xxx__
Rrk_print( str )
*/
上記では、まずヒアドキュメントの外側に「/*」がありますが、 これによってコメントアウトされる範囲は、この場合${...}内にある「*/」の部分までです (Rrk_print関数の呼び出しの後にある「*/」ではありません)。 外側から全体をコメントアウトしたい場合は「/*」と「*/」ではなく「#if 0」と「#endif」で囲いましょう。
上記ではそもそもヒアドキュメントの開始パターンのより先に「/*」が現れているため、「/*」の効果が優先され、
ヒアドキュメントの開始パターン自体が単なるコメントとなり、つまりヒアドキュメント自体が存在しないことになります。
あまりない状況とは思いますが、一応想定できます。 例えば以下をご覧下さい。
const i=9
#if 1
const str = @$|__xxx__'
AAA
${
#else
const str = @$|__xxx__'
aaa
${
#endif
i}
BBB
'__xxx__
Rrk_print( str )
「あまりない状況」どころかもはや奇妙な状況ですが、ここでは強引に想定しましょう。 一応、「#if 1」ならば、ヒアドキュメントの前半の値を「AAA」に、 「#if 0」ならば、ヒアドキュメントの前半の値を「aaa」にする意図を持って書いたものとします。
この場合、ヒアドキュメントの開始パターンは解釈されますので、まずはヒアドキュメント全体の範囲を確定する処理を行いますが、 ただし「@$」と指定されていますので同時に変数展開も行われます。
「#if 1」の場合、「AAA」の直後の「${」より子となる字句解析が行われます。 このとき親の持つ「#if 1」という情報を子が引継ぎます。 従って上の例では、子の字句解析の開始時に「#else」というトークンがいきなり現れても、 期待通りの処理を行うことができます。 この場合の実行結果は以下のようになります。
AAA
10
BBB
上の例で、もしも「#if 0」であった場合も同様にうまくいきます。 この場合は、まず「#else」まで普通にスキップし、「aaa」の直後の「${」より子となる字句解析が行われます。 このとき親の持つ「#if 0」「#else」という情報を子が引継ぎます。 従って上の例では、子の字句解析の開始時に「#endif」というトークンがいきなり現れても、 期待通りの処理を行うことができます。 この場合の実行結果は以下のようになります。
aaa
10
BBB
もっとも、(今回は挙動の説明のため敢えてこのような書き方をしましたが)普通はこのような書き方をすべきではありません。 するにしても以下のようにヒアドキュメントの外と「${...}」の内部を跨がないように工夫した方がまだ幾分は見やすいかもしれません。
const i=9
const str = @$|__xxx__'
${
#if 1
"AAA"
#else
"aaa"
#endif
}
${i}
BBB
'__xxx__
Rrk_print( str )
const i=9
#if 1
const prefix="AAA"
#else
const prefix="aaa"
#endif
const str = @$|__xxx__'
${prefix}
${i}
BBB
'__xxx__
Rrk_print( str )
あまりない状況とは思いますが、一応想定できます。 例えば以下をご覧下さい。
const val = 10
const str = @$|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return ${ val // This is a value. };
}
'__xxx__
Rrk_print( str )
上記の例では、ヒアドキュメントの${...}内に「//」で一行コメントアウトしています。 通常、「//」で一行コメントアウトをするとその効果は行末まで続き、 ファイルの最終行で「//」を使った場合、仮にそのファイルの文末に改行がなくともそこでコメントアウトの範囲は終わります。 上記は「${」から辿って一番最初に現れる「}」(「;」の直前にある「}」)が、見方によっては ${...} の終わりの記号「}」と見えなくもありません。 しかし実際には、これは「//」によるコメントアウト内の単なるコメント文字列の一部です。 つまりRarakuの字句解析器はこの「}」や行末の「;」をコメントとしてスルーし、 次の行にある「}」に至ってそれを${...}の終わりの記号「}」とみなすことになります。 そのため、実行結果は以下のように途中で途切れた感じになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 10
ヒアドキュメントを「$」を指定して始めた場合、デフォルトでは文字列リテラル内部で「${識別子}」というパターンが展開されます。 しかしこれでは文字列リテラルの中に元々「${」というパターンが含まれている場合(それを展開せずそのまま文字通り解釈して欲しい場合)に問題となります。 その場合EmbedEscape設定部において、「$」の替わりに「$=」を指定し、 埋め込みの部分を「$={...}」のように指定します。 例えば以下の通りです。
const val = 10
const str = @$=__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return $={val};
}
'__xxx__
Rrk_print( str )
上記で「${」の替わりに「$={」が変数埋め込みの開始パターンに変更されました。 では文字列リテラルの中に字面通り「${」と「$={」の両方のパターンが含まれている場合はどうすればよいでしょうか?
その場合EmbedEscape設定部において、「$=」の替わりに「$==」を指定し、 埋め込みの部分を「$=={...}」のように指定します。 例えば以下の通りです。
const val = 10
const str = @$==__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return $=={val};
}
'__xxx__
Rrk_print( str )
このように「$」の後ろに必要に応じて「=」を追加することで、パターンの衝突を回避できます。 あるいは「$」の替わりに「%」「&」に変更することもできます。 その場合も同様に「%=」、「&=」などといった指定も可能です。
変数埋め込みとエスケープ文字は同時に指定することができます。 変数埋め込みの開始パターンを「$」に、エスケープ文字を「%」に指定したい場合は、 EmbedEscape設定部において、「$,%」と指定します。 間にカンマが必要なことに注意してください。また「$,%」の間に余計な空白文字などを入れてはいけません。 例えば以下の通りです。
const val = 10
const str = @$,%__xxx__'
---%n+++%%---
aaa${val}bbb
'__xxx__
Rrk_print( str )
実行結果は以下のようになります。
---
+++%---
aaa10bbb
上記の例ではインデントに関する指定子を何も入れていませんので、インデント文字もそのまま残っています。 「%n」の直前にはインデント文字がありませんのでこの部分のインデントレベルは0とみなされることに注意してください。 つまり上記は以下のように書いたのと全く同じことです。
const val = 10
const str = @$,%__xxx__'
---
+++%%---
aaa${val}bbb
'__xxx__
Rrk_print( str )
そのため、この例では単に「|」を一つ指定しただけではインデント文字は削除されないことになります。 このインデント文字を削除したい場合、おそらく一番よいのは「|||-」と指定することでしょう。 すなわちまず「|||」により終了パターン「'__xxx__」のインデントレベル分を削除し、さらに1レベル分を追加で削除するといった形になります。
ただしEmbedEscape指定部とIndent設定部と併用して指定しますので、何度も繰り返しになりますがその順番に注意してください。 例えば「$,%」と「|||-」の併用では必ず「$,%|||-」のように指定します(「|||-$,%」のように逆順に指定した場合はコンパイルエラーとなります)。 またこの例において、「$」の替わりに「$=」と指定したい場合は「$=,%|||-」のようになります。 全体としては以下の通りです。
const val = 10
const str = @$=,%|||-__xxx__'
---%n+++%%---
aaa$={val}bbb
'__xxx__
Rrk_print( str )
実行結果は以下のようになります。
---
+++%---
aaa10bbb
変数埋め込みの開始パターンとエスケープ文字はそれぞれ「$」「%」「&」の三つのうちから 好きなものを選択できます。 ただし変数埋め込みの開始パターンとエスケープ文字は同じであってはいけません。 例えば「$,$」などといった指定をするとコンパイルエラーとなります。
ところでそもそもこのような埋め込みを使わなくても、
以下のように一旦ヒアドキュメントを閉じ、その外部で変数valを(文字列として)連結させ、
その直後でもう一度ヒアドキュメントを再開すれば同様のことは可能です
(ただし外部での連結では、全体を括弧で括るか「&」演算子を使うなどする必要があります)。
これを実行した結果は以下になります。
特に問題ないように見えます。 ただし上記は「|」を指定していないため、行頭のタブは自動的にはカットされません。
一方「|」を指定したヒアドキュメントの場合、このように外部で連結する方法ですと問題が発生します。 例えば以下をご覧ください。
これを実行した結果は以下のようになります(「return 10;」の次の行でインデントが崩れてしまいます)。
前半の「return 10;」までが一つ目のヒアドキュメントでの表示であり、 ここまでは(「|」を指定した場合のアルゴリズムにより)除去されるタブ数は2となります。 二つ目のヒアドキュメントでは「;」行においてタブが一つも含まれていません(個数0です)ので、 除去されるタブ数は0となります。 結果、後半では余分なタブが二つ残ったままになるわけです。
この問題はヒアドキュメントを二つに分離したことにより、「|」による調整に整合性がとれなくなってしまうことで発生します。 そこで「|」を指定したヒアドキュメントの場合はこのような分離を避け、 やはり以下のように変数の埋め込みを行う方がよいでしょう。
上記の例では「|」と「$」を同時に使用していますが、これは特に問題ありません。 これを実行した結果は以下のようになります。
これは望み通りの結果です。
const val = 10
const str = ( @__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return '__xxx__ val @__xxx__';
}
'__xxx__ )
Rrk_print( str ) /* 結果表示 */
これを実行した結果は以下になります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 10;
}
特に問題ないように見えます。 ただし上記は「|」を指定していないため、行頭のタブは自動的にはカットされません。
一方「|」を指定したヒアドキュメントの場合、このように外部で連結する方法ですと問題が発生します。 例えば以下をご覧ください。
const val = 10
const str = ( @|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return '__xxx__ val @|__xxx__';
}
'__xxx__ )
Rrk_print( str )
これを実行した結果は以下のようになります(「return 10;」の次の行でインデントが崩れてしまいます)。
#include <stdio.h>
int main( int argc, char** argv )
{
return 10;
}
前半の「return 10;」までが一つ目のヒアドキュメントでの表示であり、 ここまでは(「|」を指定した場合のアルゴリズムにより)除去されるタブ数は2となります。 二つ目のヒアドキュメントでは「;」行においてタブが一つも含まれていません(個数0です)ので、 除去されるタブ数は0となります。 結果、後半では余分なタブが二つ残ったままになるわけです。
この問題はヒアドキュメントを二つに分離したことにより、「|」による調整に整合性がとれなくなってしまうことで発生します。 そこで「|」を指定したヒアドキュメントの場合はこのような分離を避け、 やはり以下のように変数の埋め込みを行う方がよいでしょう。
const val = 10
const str = ( @$|__xxx__'
#include <stdio.h>
int main( int argc, char** argv )
{
return ${val};
}
'__xxx__ )
Rrk_print( str )
上記の例では「|」と「$」を同時に使用していますが、これは特に問題ありません。 これを実行した結果は以下のようになります。
#include <stdio.h>
int main( int argc, char** argv )
{
return 10;
}
これは望み通りの結果です。
事例:HTMLをヒアドキュメントで書く
ここからはより実践的な事例を見ていきましょう。 最終的に以下のような出力となるHTMLを記述したいとします。
<hr>
<b>My Table</b>
<table border=0><tr><th>Idx</th><th>Res</th><th>No</th><th>Thumb</th><th>Comment</th></tr></table>
「My Table」の部分はtitle変数で与えることにしましょう。 また今、上記でtableタグの部分は全部一行で出力しなければならない制約があるとしましょう。 この場合、これを出力するRarakuのコードは例えば以下のようになります。
const title = "My Table"
const str = @$|['
<hr>
<b>${title}</b>
<table border=0><tr><th>Idx</th><th>Res</th><th>No</th><th>Thumb</th><th>Comment</th></tr></table>
']
Rrk_print( str )
しかしコーディングする側から見ると、これではtableタグがすべて一行で書かれることになり、階層構造が一目ではわかりにくいです。 最終的な出力は一行でなければならないという制約がありますから、途中に余計なインデントや改行を入れるわけにもいきません。 それでもなんとかインデントをして構造を見やすくして書こうとするならば、例えば以下のように敢えて全体を一つのヒアドキュメントで書くのを避け、 一行毎に分解して書く手もあります。
const title = "My Table"
const str = (
"<hr>"\n
"<b>"title"</b>"\n
"<table border=0>"
"<tr>"
"<th>Idx</th>"
"<th>Res</th>"
"<th>No</th>"
"<th>Thumb</th>"
"<th>Comment</th>"
"</tr>"
"</table>"\n
)
Rrk_print( str )
上記では一応(コード上は)インデントされましたので、階層構造は見やすくなりましたが、 しかし今度はすべての行を「"」で囲う手間がかかります。 これを回避するには、やはり全体を一つのヒアドキュメントで書く方がよいわけです。
全体を一つのヒアドキュメントで書く場合に問題になるのは行頭に入る余計なインデントと、行末に入る余計な改行です。 要するにこれらを除去できればよいわけです。 ただし、この例では<hr>の直後と</b>の直後には改行が必要です。 そのためにまずは「||」を指定することで行頭のインデントを完全に除去し、「/""」を指定することで行末の改行を完全に除去させます。 さらにはエスケープ文字として「%」などを指定し、必要な所には「%n」を付加すればよいでしょう。 例えば以下のようになります。
const str = @$,%||/""['
<hr>%n
<b>${title}</b>%n
<table border=0>
<tr>
<th>Idx</th>
<th>Res</th>
<th>No</th>
<th>Thumb</th>
<th>Comment</th>
</tr>
</table>%n
']
Rrk_print( str )
「"」で囲う必要もなくなり、かなりすっきりしました。
「"」で一行毎に分解して書く方法が必ずしも悪手かと言えば、そうでもありません。
ヒアドキュメントは強力ではあるものの、事前に指定子に複雑な指定をするなどの準備が必要です。
上記の例では中身がそれなりに行数があったため、ヒアドキュメントが効力を発揮しましたが、 これがもし行数がたかだか一行か二行で、しかも中身に「"」などが使われていないようなシンプルな文章ならば、 むしろ各行を「"」で括った方が結局字数も少なく簡潔になる場合もあります。
変数埋め込みも同様で、「${」「}」の三文字を使うより、「"」「"」の2文字で一旦文字列リテラルを分断した方が 結局短く書けるということもあります。
適材適所で一番よいと思われるものを選びましょう。
上記の例では中身がそれなりに行数があったため、ヒアドキュメントが効力を発揮しましたが、 これがもし行数がたかだか一行か二行で、しかも中身に「"」などが使われていないようなシンプルな文章ならば、 むしろ各行を「"」で括った方が結局字数も少なく簡潔になる場合もあります。
変数埋め込みも同様で、「${」「}」の三文字を使うより、「"」「"」の2文字で一旦文字列リテラルを分断した方が 結局短く書けるということもあります。
適材適所で一番よいと思われるものを選びましょう。
もう一つの例を見ましょう。 最終的に以下のような出力となるHTMLを記述したいとします。
<a class=MstyElemLink href="javascript:submitBtn('fm_main_id', 'manager=img_viewer&command=stock&auth_key=1234&config=on');" target=_blank>Stock</a>
かなり複雑なリンクの記述ですので、「manager」、「command」、「auth_key」「config」の部分は埋め込み変数で与えることにしましょう。 また今、上記は全部を一行で出力しなければならない制約があるとしましょう(最後に改行はします)。 これを何も改行せず本当にそのままで記述すると以下のようになります。
const
manager = "img_viewer",
command = "stock",
auth_key = "1234",
config = "on",
;
const str = @$|['
<a class=MstyElemLink href="javascript:submitBtn('fm_main_id', 'manager=${manager}&command=${command}&auth_key=${auth_key}&config=${config}');" target=_blank>Stock</a>
']
Rrk_print( str )
「manager=${manager}」以降の部分が猛烈に見難く、できればインデントと改行をしたいところです。 先ほど紹介したやり方を使って(コード上だけの)インデントと改行をすると以下のようになります。
const
manager = "img_viewer",
command = "stock",
auth_key = "1234",
config = "on",
;
const str = @$||/""__nb_html__'
<a class=MstyElemLink
${} href="javascript:
submitBtn('fm_main_id',
${} '${\=manager}
&${\=command}
&${\=auth_key}
&${\=config}
'
);
"${ /* endof href */ }
${} target=_blank
>
Stock
</a>
'__nb_html__\n
Rrk_print( str )
上記ではまず最初に「@$||/""」と指定子がありますので、「$」による変数埋め込みが有効であり、 行頭のインデントは問答無用で消去し、行末の改行は""に変換(つまり消去)します。 __nb_html__は別になくても構いませんがここではコメント的な役割として敢えて付けています。
中身の文字列中の「${}」は空文字に展開される特別な変数埋め込みです。 そのため結果の出力としては何の意味もないのですが、この直前にあるタブ文字と 直後にある半角のスペース(MstyElemLinkとhrefの間に入る半角スペースです)との区切りが紛らわしいため、これを分離する意味でおいています。
「${\=manager}」は変数埋め込みの内部でさらにバックスラッシュ演算子を使っており、中で「"manager=" & manager」と展開されますから 全体としては「manager=${manager}」と書いたのと同じです。 もちろんそれがわかりにくければ普通に「manager=${manager}」と書いてもよいでしょう。 「${\=command}」、「${\=auth_key}」、「${\=config}」についても同様です。
「${ /* endof href */ }」はコメントです。「${...}」の中には単にコメントだけを書くことも許されます。 その場合、ここは「${}」の場合と同じく空文字に展開されます。 わざわざ「${...}」を用意してその中に書いたのは、今回は結果の出力にコメントを含めてはならないためです。
「>」「Stock」「</a>」の部分は、今回は極力構造をはっきりさせる書き方をしましたが、 これくらいならまとめて一行で「>Stock</a>」と書いてもよいでしょう。 いずれにしてもこれらの前後のインデントや改行は削除されるため、結果の出力は同じです。
最後に(ヒアドキュメントの外側に)\nを置いていますが、ヒアドキュメントと\nはどちらも文字列リテラルであるため、この間の&は省略可能です。 紛らわしいようであれば&を付けるか全体を括弧で括ってももちろん構いません。 あるいは最初に「@$,%||""」と指定して「%」をエスケープ文字として有効化し、 ヒアドキュメント内部の最後の位置に「%n」を指定するなどしてもよいでしょう。
上記は、「manager」、「command」、「auth_key」「config」の部分は埋め込み変数で与えるという条件があったため、 敢えてこのような書き方をしましたが、 実際は、「${\=manager}」、「${\=command}」、「${\=auth_key}」、「${\=config}」の連結の部分は一旦ヒアドキュメントの外で 予め作っておく方がより見やすいでしょう。 例えば以下の通りです。
const
manager = "img_viewer",
command = "stock",
auth_key = "1234",
config = "on",
;
const am = "&"
const path_arg = (
\=manager
&am \=command
&am \=auth_key
&am \=config
)
const str = @$||/""__nb_html__'
<a class=MstyElemLink
${} href="javascript:
submitBtn('fm_main_id', '${path_arg}');
"
${} target=_blank
>
Stock
</a>
'__nb_html__\n
Rrk_print( str )
上記で「am」ではなく「&am」と書いてあるのは「\=manager」と「am」の間の文字列連結演算子「&」は省略できないためです。 それ以降にある「&am」も同様です。
「\=manager」は「"manager=" & manager」と展開されますが、このとき最後に「manager」という識別子があり、
これに続いて「am」という識別子が連続するため、この間の文字列連結演算子「&」は省略できません。
事例:HTTPヘッダをヒアドキュメントで書く
HTTPヘッダはCGIなどで記述することがあります。 あるいはソケット通信などでHTMLをダウンロードしたり、サイトへコメントをポストする場合も、 この情報は必要になります。
今回は最終的に以下のような出力となるHTTPヘッダを記述したいとします。
GET /b/res/index.html HTTP/1.1
Host: www.example.net
User-Agent: FireMoai
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
上記で、GETの第一引数は変数「urp」、Host:の引数は変数host、User-Agentの引数は変数uaで与えるものとします。 またHTTPヘッダの改行コードは厳密に必ず\r\nで改行する必要があります。 さらに最終行に一つ空行が必要です(つまり最後に二回(\r\nで)改行されます)。 つまり、ファイルの改行コードに依存するような書き方ではなく、明示的に\r\nを指定しなければなりません。 これらを踏まえますと、上記を出力するRarakuのコードは、例えば以下のようになるでしょう。
const host = "www.example.net"
const urp = "/b/res/index.html"
const ua = "FireMoai"
const str = (
"GET " urp " HTTP/1.1"\r\n
"Host: " host \r\n
"User-Agent: " ua \r\n
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"\r\n
"Accept-Language: ja,en-US;q=0.7,en;q=0.3"\r\n
"Accept-Encoding: gzip, deflate"\r\n
"Connection: keep-alive"\r\n
\r\n
)
/* dummy socket function */
function DmySocket_send_cstr( data conststr ){
Rrk_print( data ) /* anyway */
}
DmySocket_send_cstr( str )
HTTPヘッダをCGIでRrk_printする場合、本来はまずRrk_setMode関数で標準出力をバイナリモードに変更しなければなりません。
またソケット通信では通信用の関数(Raraku Extensの一つであるKau HtpBoyなどを使用します)を呼ぶ必要があります。
上記のHTTPヘッダは内容からしてソケット通信用を想定したものですが、 それを説明するとそれだけで一つのセクションになってしまいます。 今回はHTTPヘッダそのものを作ることが主眼ですのでそれについての説明は割愛し、 最後に何もせず単にRrk_printを読んで表示させるだけにしてあります。
上記のHTTPヘッダは内容からしてソケット通信用を想定したものですが、 それを説明するとそれだけで一つのセクションになってしまいます。 今回はHTTPヘッダそのものを作ることが主眼ですのでそれについての説明は割愛し、 最後に何もせず単にRrk_printを読んで表示させるだけにしてあります。
上記はそこまで見難いというほどではありませんが、HTMLの時の例と同様にヒアドキュメントの指定子を工夫すれば、 各行毎の「"」や\r\nを省略して書くことも可能です。 例えば以下の通りです。
const host = "www.example.net"
const urp = "/b/res/index.html"
const ua = "FireMoai"
const hdr = @$|/"\r\n"__http__'
GET ${urp} HTTP/1.1
Host: ${host}
User-Agent: ${ua}
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
'__http__
/* dummy socket function */
function DmySocket_send_cstr( data conststr ){
Rrk_print( data ) /* anyway */
}
DmySocket_send_cstr( str )
HTTPヘッダのインデントの削除については、「|」と「||」のどちらでも構いません。 HTTPヘッダの場合、ある行が他の行と比べてインデントが深いといったような書き方はせず、 とりあえず全行の開始の列位置は揃っているからです。
むしろ問題は改行コードにあります。 ヒアドキュメントでは生の改行コードは\nに変換されますが、今回はそれをそのままにしておくわけにはいきませんので、 LineBreak設定部において「/"\r\n"」を指定します。 これにより、(ヒアドキュメント内の範囲において)改行コードが強制的に"\r\n"に置換されますので、 コーディングの上では(ファイルの改行コードが何であれ)単に改行するだけでそこに「\r\n」が明示的に指定されるのと同じ効果となります。 HTTPヘッダでは最後に一つ「\r\n」が必要ですが、この場合これは空行で対処できます。
今回HTTPヘッダやCGI、またそれに指定する改行コードの話が出ましたので、これに関して若干補足をしておきましょう。
CGIではHTTPヘッダを(改行を厳密に\r\nとして)標準出力する必要がありますが、 これをWindowsとLinuxの両方で移植性のある書き方をするのは案外厄介です。 結論から言えばこのためにはまずはRrk_setMode関数を使い、その後でRrk_printなどを使うことになるでしょう。
まず"\n"という文字列リテラルとは何かについて改めて考えましょう。 "\n"という文字列リテラルを単に書いた時点では、それは("\r\n"ではなく)本当に"\n"です。 実際、RrkStr_lengで "\n" の文字列長を調べるとその値は(Windowsであっても)1になります。
Windowsでは内部でそれを「\r\n」に変換しますが、 これは正確には(テキストモードでの)標準出力や(テキストモードでの)ファイル出力をする関数に「\n」を与えた場合です。 例えばRrk_print関数も、特に何も設定せずに使った場合はこのような関数に該当します。 以下をご覧下さい。
上記の呼び出しでは、Windowsにおいては"\n"がRrk_print内部で「\r\n」に変換されてコマンドプロンプトに表示されます。 一方、Linuxでは「\n」のままでターミナルに表示されます。
Linuxにおいて「\r\n」と出力させたい場合、とりあえず以下のように指定すれば一応は可能です(ただしこれには問題があります)。
この呼び出しは確かにLinuxに限定すれば意図通りの出力となりますが、 これをそのままWindows環境に持っていくと、(特に何も対処しなければ)おそらくプログラマの意図した出力とはなりません。 繰り返しになりますがWindowsでは上記の呼び出しによって、内部で"\n"が「\r\n」に変換されます。 ということは、(Windowsでは)"\r\n" が「\r\r\n」といった変な改行に変換されてしまうということです。
Windowsでのこの変換を抑制するためにはRrk_setMode関数を使い、標準出力をバイナリモードに変換します。 例えば以下の通りです。
要するにバイナリモードとは余計な変換をせず、それを文字通り出力するためのモードです (Windowsのデフォルトはテキストモードと呼ばれ、このテキストモードの状態では"\n"は内部で「\r\n」に変換されます)。 バイナリモードに変換した後であれば、("\n"から「\r\n」への)変換が行われなくなり、 上記で正確に「\r\n」を出力させることができます。
尚、Linuxでは常にバイナリモードです(そもそもテキストモードというものが存在しません)。 Rrk_setModeでは、実行環境がLinuxの場合は何もしないようになっています。 そのため、とりあえずこれを呼び出しておけば、外見上は同じコードとして書けるというわけです (このような関数をラッパーと呼びます)。
CGIではHTTPヘッダを(改行を厳密に\r\nとして)標準出力する必要がありますが、 これをWindowsとLinuxの両方で移植性のある書き方をするのは案外厄介です。 結論から言えばこのためにはまずはRrk_setMode関数を使い、その後でRrk_printなどを使うことになるでしょう。
まず"\n"という文字列リテラルとは何かについて改めて考えましょう。 "\n"という文字列リテラルを単に書いた時点では、それは("\r\n"ではなく)本当に"\n"です。 実際、RrkStr_lengで "\n" の文字列長を調べるとその値は(Windowsであっても)1になります。
Windowsでは内部でそれを「\r\n」に変換しますが、 これは正確には(テキストモードでの)標準出力や(テキストモードでの)ファイル出力をする関数に「\n」を与えた場合です。 例えばRrk_print関数も、特に何も設定せずに使った場合はこのような関数に該当します。 以下をご覧下さい。
Rrk_print( "\n" )
上記の呼び出しでは、Windowsにおいては"\n"がRrk_print内部で「\r\n」に変換されてコマンドプロンプトに表示されます。 一方、Linuxでは「\n」のままでターミナルに表示されます。
Linuxにおいて「\r\n」と出力させたい場合、とりあえず以下のように指定すれば一応は可能です(ただしこれには問題があります)。
Rrk_print( "\r\n" )
この呼び出しは確かにLinuxに限定すれば意図通りの出力となりますが、 これをそのままWindows環境に持っていくと、(特に何も対処しなければ)おそらくプログラマの意図した出力とはなりません。 繰り返しになりますがWindowsでは上記の呼び出しによって、内部で"\n"が「\r\n」に変換されます。 ということは、(Windowsでは)"\r\n" が「\r\r\n」といった変な改行に変換されてしまうということです。
Windowsでのこの変換を抑制するためにはRrk_setMode関数を使い、標準出力をバイナリモードに変換します。 例えば以下の通りです。
Rrk_setMode( RrkFileDescriptor_e_stdout, true ) /* for binary mode */
Rrk_print( "\r\n" ) /* OK */
要するにバイナリモードとは余計な変換をせず、それを文字通り出力するためのモードです (Windowsのデフォルトはテキストモードと呼ばれ、このテキストモードの状態では"\n"は内部で「\r\n」に変換されます)。 バイナリモードに変換した後であれば、("\n"から「\r\n」への)変換が行われなくなり、 上記で正確に「\r\n」を出力させることができます。
尚、Linuxでは常にバイナリモードです(そもそもテキストモードというものが存在しません)。 Rrk_setModeでは、実行環境がLinuxの場合は何もしないようになっています。 そのため、とりあえずこれを呼び出しておけば、外見上は同じコードとして書けるというわけです (このような関数をラッパーと呼びます)。
ヒアドキュメントのまとめ
Rarakuのヒアドキュメントでは、多くの場合「@|」を指定した書式で、 (たとえインデント下であっても)望み通りの結果になると思います。 その上で「+」や「-」指定子によりインデントの微調整をするとよいでしょう。 あるいは「!」を指定した上で「+」や「-」指定子を付けて、元のインデントレベルから相対的に増減する方法もあります。
ヒアドキュメント内にRarakuの式を埋め込んでその実行結果で文字列展開したい場合は、 「@$|」などを指定すると大抵の場合便利でしょう。 この場合、中身の文字列においては ${...} を埋め込み、「...」の部分にRarakuの式を書きます。
ヒアドキュメント内の生の改行はデフォルトでは\nに変換されます。 これを\r\nに変換したい場合はLineBreak設定部において「/"\r\n"」を指定するとよいでしょう。 これを「$」や「|」と併用する場合はその順番に注意します。 EmbedEscape設定部の「$」が一番初めであり、Indent設定部の「|」が二番目、LineBreak設定部の「/"\r\n"」が一番最後になります。
目次に戻る
ビット演算子
ビット演算子の概要
Rarakuのビット演算子はすべて「^b_」で始まります。 ただしビット和だけは「^b_or」の替わりに「|」と書くこともできます。
ビット和だけ短く「|」という表記を特別に用意したのは、他の演算子と比べてこれを多く連結するような用途が多いと考えられるからです。
尚、ビット和はビット論理和やビット単位ORなどと呼ばれることもありますが、この記事では短く「ビット和」という用語を使うことにします。 ビット積についても同様です。
尚、ビット和はビット論理和やビット単位ORなどと呼ばれることもありますが、この記事では短く「ビット和」という用語を使うことにします。 ビット積についても同様です。
ビット演算子は大きく、二項演算子、単項演算子、複合代入演算子の三つに分類できます。
参考: C言語との違い
C言語をご存知の方はこの参考を見た方が手っ取り早いでしょう。
ご存知ない方はこの参考は読み飛ばしてください。
Rarakuのビット演算子はビット和を除きC言語の表記とは全く異なり、以下のようになります。
Rarakuでは、ビット和は「|」と「^b_or」のどちらを使っても構いません(この二つは全く同じ意味です)。 しかしRarakuでは「&」は文字列連結を行うための二項演算子になりますので、 ビット積については必ず「^b_and」を使用してください。
「^b_shift_l」「^b_shift_r」は二項演算子であることに注意しましょう。 Rarakuでは、C言語で言うところの複合代入演算子「<<=」、「>>=」は「^b_set_shift_l」「^b_set_shift_r」となります。 例えば以下の通りです。
これ以外にもRarakuのビット演算子で複合演算子になっているものは一部の例外を除きすべて「^b_set」または「^b_unset」で始まります。
例えばC言語での「|=」は単に「^b_set」となります。 C言語での「&=」は「^b_set_and」となりますが、これについてはもっと短く「^b_filt」と書くこともできます (この演算子は多くの場合ビットが1になっている部分だけを残すフィルタリングの用途で使用するため、 それを連想しやすい名前としてあります)。 因みにRarakuにおいて「&=」は文字列連結を行うための複合代入演算子になりますので、 ビット積の複合代入演算子については必ず「^b_filt」か「^b_set_and」を使用してください。
「^b_unset」はRaraku独自の複合代入演算子です (これに相当するC言語の欄の「&=~」は、正確には単独の演算子ではなく「&=」と「~」を組み合わせたものになります)。
「^b_is」はRaraku独自の二項演算子であり、その値はビット積「^b_and」をとったものが非ゼロならばtrue、さもなければfalseとなります (演算結果はbool型です)。例えば以下の通りです。
上記でbval1、bval2、bval3の左辺はいずれも実質的に同じ処理になります。 「->bool」はbool型へのキャストを意味します(ただしRarakuでのbool型へのキャストはC言語の意味でのそれとは若干処理が異なり、 非ゼロである場合true、ゼロである場合falseとなるような明確な変換が内部で行われます)。 「^b_is」ではこれを書く手間を省けます(逆に言えばそれだけの違いです)。 またif文等の条件式として与えるならば「^b_and」を使った表記であってもbool型にキャストする必要はありません。 つまりこの場合に限り以下のように表記しても構いません。
ビット否定演算子についてはオペランドで与えられた整数のビット長を意識しなければならないケースもあるため、 Rarakuでは32bit版の「^b_not32」と64bit版の「^b_not64」の二つが用意されています。 例えば64bit整数を与えてこれを「^b_not32」で処理した場合、下位32bitでのみビット否定を行うといったようなことができます (C言語的にはこれは一旦 uint32_t 型へキャストしてから「~」演算子を適用することに相当します)。
Close
Rarakuのビット演算子はビット和を除きC言語の表記とは全く異なり、以下のようになります。
| C言語 | Raraku | 演算子の種類 |
|---|---|---|
| | | | または ^b_or | 二項演算子(ビット和) |
| & | ^b_and | 二項演算子(ビット積) |
| ^ | ^b_xor | 二項演算子(Exclusive OR) |
| << | ^b_shift_l | 二項演算子(左シフト) |
| >> | ^b_shift_r | 二項演算子(右シフト) |
| 存在しない | ^b_is | 二項演算子(ビット積の結果が非ゼロならtrue, さもなくばfalse) |
| ~ | ^b_not32 | 単項演算子(32bit整数とみなして否定演算) |
| ~ | ^b_not64 | 単項演算子(64bit整数とみなして否定演算) |
| |= | ^b_set | 複合代入演算子(ビット和) |
| &= | ^b_filt または ^b_set_and | 複合代入演算子(ビット積) |
| &=~ | ^b_unset | 複合代入演算子(1の部分のビットを消去) |
| ^= | ^b_set_xor | 複合代入演算子(Exclusive OR) |
| <<= | ^b_set_shift_l | 複合代入演算子(左シフト) |
| >>= | ^b_set_shift_r | 複合代入演算子(右シフト) |
ご覧のようにC言語より表記は簡潔ではありません。
ビット演算子の使用頻度は(ジャンルにもよりますが)あまり高いものではないと思われるため
(特にExclusive ORなどあまり使わないと思いますが)、Rarakuでは意味を重視した表記としてあります。
Rarakuでは、ビット和は「|」と「^b_or」のどちらを使っても構いません(この二つは全く同じ意味です)。 しかしRarakuでは「&」は文字列連結を行うための二項演算子になりますので、 ビット積については必ず「^b_and」を使用してください。
「^b_shift_l」「^b_shift_r」は二項演算子であることに注意しましょう。 Rarakuでは、C言語で言うところの複合代入演算子「<<=」、「>>=」は「^b_set_shift_l」「^b_set_shift_r」となります。 例えば以下の通りです。
varia flag uint32 = 0x01
flag ^b_shift_l 3 /* flagを3bit左シフトしたものを計算(flagは更新されない) */
flag ^b_set_shift_l 3 /* flagを3bit左シフトしたものをflagに代入 */
flag ^b_shift_r 2 /* flagを2bit右シフトしたものを計算(flagは更新されない) */
flag ^b_set_shift_r 2 /* flagを2bit左シフトしたものをflagに代入 */
これ以外にもRarakuのビット演算子で複合演算子になっているものは一部の例外を除きすべて「^b_set」または「^b_unset」で始まります。
例えばC言語での「|=」は単に「^b_set」となります。 C言語での「&=」は「^b_set_and」となりますが、これについてはもっと短く「^b_filt」と書くこともできます (この演算子は多くの場合ビットが1になっている部分だけを残すフィルタリングの用途で使用するため、 それを連想しやすい名前としてあります)。 因みにRarakuにおいて「&=」は文字列連結を行うための複合代入演算子になりますので、 ビット積の複合代入演算子については必ず「^b_filt」か「^b_set_and」を使用してください。
「^b_unset」はRaraku独自の複合代入演算子です (これに相当するC言語の欄の「&=~」は、正確には単独の演算子ではなく「&=」と「~」を組み合わせたものになります)。
「^b_is」はRaraku独自の二項演算子であり、その値はビット積「^b_and」をとったものが非ゼロならばtrue、さもなければfalseとなります (演算結果はbool型です)。例えば以下の通りです。
varia flag uint32 = 0x03
varia mask uint32 = 0x01
/* 以下はどれも同じbool値となる */
varia bval1 = flag ^b_is mask
varia bval2 = (flag ^b_and mask) != 0
varia bval3 = (flag ^b_and mask)->bool
上記でbval1、bval2、bval3の左辺はいずれも実質的に同じ処理になります。 「->bool」はbool型へのキャストを意味します(ただしRarakuでのbool型へのキャストはC言語の意味でのそれとは若干処理が異なり、 非ゼロである場合true、ゼロである場合falseとなるような明確な変換が内部で行われます)。 「^b_is」ではこれを書く手間を省けます(逆に言えばそれだけの違いです)。 またif文等の条件式として与えるならば「^b_and」を使った表記であってもbool型にキャストする必要はありません。 つまりこの場合に限り以下のように表記しても構いません。
varia flag uint32 = 0x03
varia mask uint32 = 0x01
/* 以下はどちらも同じ条件式が与えられたものと考えてよい */
if flag ^b_is mask {
/* something */
}
if flag ^b_and mask { /* この場合は自動的にbool値へ変換される */
/* something */
}
ビット否定演算子についてはオペランドで与えられた整数のビット長を意識しなければならないケースもあるため、 Rarakuでは32bit版の「^b_not32」と64bit版の「^b_not64」の二つが用意されています。 例えば64bit整数を与えてこれを「^b_not32」で処理した場合、下位32bitでのみビット否定を行うといったようなことができます (C言語的にはこれは一旦 uint32_t 型へキャストしてから「~」演算子を適用することに相当します)。
Close
ビットの二項演算子:ビット和とビット積
Rarakuのビット演算子のうち、二項演算子(オペランドを二つとる演算子)について解説します。 まずはビット演算の中では比較的使われる頻度が高いと思われるビット和です。 以下の例をご覧下さい。
varia u uint
/* ビット和 */
u = 0x02 | 0x03 /* u = 0x03 */
上記で「0x02」や「0x03」となっている部分は16進数を表すuint型リテラルです。 この例ではこれらのビット和をとっていますので結果は「0x03」となります。
補足: ビット和とは?
ビット和についても念のため補足しておきます(ご存知の方はこれは読み飛ばしてください)。
「0x02」や「0x03」を2進数で書くとそれぞれ「0000 0010」「0000 0011」となります。 ビット和とは2進数で対応する位を比べた時に、そのいずれか一方でも1であれば、(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 1、
0 と 1 では 1、
1 と 1 では 1
となります。
従って「0000 0010」と「0000 0011」のビット和をとれば結果は「0000 0011」となります。 さらにこの結果を16進数で書けば「0x03」となります。
Close
「0x02」や「0x03」を2進数で書くとそれぞれ「0000 0010」「0000 0011」となります。 ビット和とは2進数で対応する位を比べた時に、そのいずれか一方でも1であれば、(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 1、
0 と 1 では 1、
1 と 1 では 1
となります。
従って「0000 0010」と「0000 0011」のビット和をとれば結果は「0000 0011」となります。 さらにこの結果を16進数で書けば「0x03」となります。
Close
次にビット積について解説します。 以下の例をご覧下さい。
varia u uint
/* ビット積 */
u = 0x11 ^b_and 0x10 /* u = 0x10 */
上記の例では「0x11」と「0x10」のビット積をとっていますので結果は「0x10」となります。
補足: ビット積とは?
ビット積についても念のため補足しておきます(ご存知の方はこれは読み飛ばしてください)。
「0x11」や「0x10」を2進数で書くとそれぞれ「0001 0001」「0001 0000」となります。 ビット積とは2進数で対応する位を比べた時に、その両方が1である場合に限り(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 0、
0 と 1 では 0、
1 と 1 では 1
となります。
従って「0001 0001」と「0001 0000」のビット積をとれば結果は「0001 0000」となります。 さらにこの結果を16進数で書けば「0x10」となります。
Close
「0x11」や「0x10」を2進数で書くとそれぞれ「0001 0001」「0001 0000」となります。 ビット積とは2進数で対応する位を比べた時に、その両方が1である場合に限り(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 0、
0 と 1 では 0、
1 と 1 では 1
となります。
従って「0001 0001」と「0001 0000」のビット積をとれば結果は「0001 0000」となります。 さらにこの結果を16進数で書けば「0x10」となります。
Close
ビット積に関係の深い、「^b_is」という演算子についてもここで紹介します。 以下の例をご覧下さい。
/* ビット積の結果をbool型へ変換したものを得る(ビット積の結果が非ゼロである場合はtrue、ゼロである場合はfalseとなる) */
varia bval bool = 0x06 ^b_is 0x02 /* bval = true */
上記は「0x06」と「0x02」を「^b_is」という演算子を用いて計算したものです。 この演算子はまず与えられた二つのオペランドのビット積をとり、 その結果が非ゼロである場合はtrue、ゼロである場合はfalseとなります (他の演算子と違い、この演算子では結果は整数型ではなくbool型となります)。 「0x06」と「0x02」のビット積は「0x02」になりますから(つまり非ゼロですから)、 最終的な結果はtrueとなります。
尚、ここまでの例では、uint型リテラルを直に書いておりますが、実際のプログラミングではこれらにある程度意味ある名前を付けた定数として それを介した記述とする方が望ましいでしょうしそれが普通でしょう。 例えば以下の通りです。
const SDL_WINDOW_FULLSCREEN = 0x00000001 /* fullscreen window */
const SDL_WINDOW_OPENGL = 0x00000002 /* window usable with OpenGL context */
varia flag uint = SDL_WINDOW_FULLSCREEN /* flag = 0x01 */
if flag ^b_is SDL_WINDOW_OPENGL {
/* something */
}
ビットの二項演算子:シフト演算子
ビットの二項演算子として左ビットシフト、右ビットシフトというものもあります。 これは左ビットシフトの場合「X ^b_shift_l Y」、右ビットシフトの場合「X ^b_shift_r Y」という形式で記述します。 この演算子ではオペランド X と Y の役割は大きく異なり、両者の順番を入れ替えることはできません(可換ではありません)。
まずは左ビットシフトから説明しましょう。
左ビットシフトは、2進数での表記において全体を左側へ指定した個数(ビット数)だけ移動するような処理を行います。 例えば、2進数表記で「0100 0101」(16進数では0x45)となる8bit unsigned整数があり、これを左ビットシフトで1bit移動した場合は 「1000 1010」(16進数では0x8a)となります。2bit移動した場合は「0001 0100」です。 このとき右端では(移動ビット数分だけ)0 で埋められます。 一方、左端では(移動ビット数分だけ)追い出されるビットが発生しますが、それは消失します。
上記の処理ををRarakuの表記で書くと「0x45 ^b_shift_l 1」のようになります。 以下が使用例となります。
/* 左ビットシフト(左側のオペランド(整数)に対し、右側のオペランド(整数)ビット分左へシフト) */
varia u = 0x45 ^b_shift_l 1 /* u = 0x8a */
ここでオペランド X においては、unsigned整数 0x45(2進数では「0100 0101」)を指定し、 オペランド Y においては、実際にシフトするビット数 1 を指定しています。 シフトするビット数が 2 の場合は、「0x45 ^b_shift_l 2」となります。
次に右ビットシフトについて説明しましょう。
右ビットシフトは、2進数での表記において全体を右側へ指定した個数(ビット数)だけ移動するような処理を行います。 例えば、2進数表記で「0100 0101」(16進数では0x45)となる8bit unsigned整数があり、これを右ビットシフトで1bit移動した場合は 「0010 0010」(16進数では0x22)となります。2bit移動した場合は「0001 0001」です。 このとき左端では(移動ビット数分だけ)0 で埋められます。 一方、右端では(移動ビット数分だけ)追い出されるビットが発生しますが、それは消失します。
上記の処理をRarakuの表記で書くと「0x45 ^b_shift_r 1」、「0x45 ^b_shift_r 2」のようになります。 以下が使用例となります。
/* 右ビットシフト(左側のオペランド(整数)に対し、右側のオペランド(整数)ビット分右へシフト) */
varia u = 0x45 ^b_shift_r 1 /* u = 0x22 */
ここまでオペランド X (左オペランド)がunsigned整数である場合のみで説明しましたが、これがsigned整数である場合、少し注意が必要です。 左ビットシフトの場合は、X がsigned整数とunsigned整数のどちらであれ、挙動は同じです。 この挙動は一般に論理シフト(logical shift)と呼ばれます。
一方、右ビットシフトの場合は、X がsigned整数とunsigned整数で挙動が変わります。 X がunsigned整数での右ビットシフトの場合、左端では(移動ビット数分だけ)0 で埋められましたが、 X がsigned整数での右ビットシフトの場合、左端では(移動ビット数分だけ)1 で埋められます。 この挙動は、論理シフトに対して特に算術シフト(arithmetic shift)と呼ばれます。
ちなみにC言語(VC/gcc)でもこれは同様の挙動になります。
C言語では、シフト演算子の右オペランド(つまりシフトするビット数)の最大値は、
左オペランド(つまりシフトされる側の整数)の整数型としてのbitサイズによって決まります。
今、左オペランドが N bit unsigned整数(Nは64, 32, 16, 8のいずれか)であるとしますと、
シフトできる最大値(右オペランドに与えることのできる最大値)は N-1 となります。
例えば、64bit整数の場合、64-1=63が最大値です。
これを超えた値(N以上の値)を指定した場合どうなるのでしょうか?
まずこのような値を整数リテラルとして指定した場合、 VC, gccともにコンパイルエラーとまではならないものの、指定されたシフト量が大きすぎるといった旨の警告が表示されます。 また変数として指定した場合、その中身にどんな値が入ってるかは実行時までわかりませんから、 この場合は警告も表示されません。 この結果がどうなるかは(この記事を執筆の時点では)C言語で厳密には規定されていませんが、 VC, gccで実際に実行させて確認してみると、両者ともにNの剰余をとった値が指定されたものとみなされて計算されます。 例えば、64bit整数の場合に、64bit左シフトさせた場合は 64%64=0 bitの左シフト、つまり何も作用しないことになりますし、 65bit左シフトさせた場合は 65%64=1 bitの左シフトと同じになります。
C言語としての厳密な規定ではありませんが、VCとgccのこの挙動はそれなりに理には適ってはいると思われますので、 Rarakuでもこの挙動に合わせています。
これを超えた値(N以上の値)を指定した場合どうなるのでしょうか?
まずこのような値を整数リテラルとして指定した場合、 VC, gccともにコンパイルエラーとまではならないものの、指定されたシフト量が大きすぎるといった旨の警告が表示されます。 また変数として指定した場合、その中身にどんな値が入ってるかは実行時までわかりませんから、 この場合は警告も表示されません。 この結果がどうなるかは(この記事を執筆の時点では)C言語で厳密には規定されていませんが、 VC, gccで実際に実行させて確認してみると、両者ともにNの剰余をとった値が指定されたものとみなされて計算されます。 例えば、64bit整数の場合に、64bit左シフトさせた場合は 64%64=0 bitの左シフト、つまり何も作用しないことになりますし、 65bit左シフトさせた場合は 65%64=1 bitの左シフトと同じになります。
C言語としての厳密な規定ではありませんが、VCとgccのこの挙動はそれなりに理には適ってはいると思われますので、 Rarakuでもこの挙動に合わせています。
16進数のuint型リテラルを直接記述する替わりにに左ビットシフトを使うこともできます。
例えば以下の通りです。
上記の定数の値はそれぞれ「0x01」「0x02」「0x04」「0x08」「0x10」「0x20」となります。 勿論これをそのまま16進数のuint型リテラルとして記述しても全く構わないのですが、 左ビットシフトを使ってこれを表現した場合、「^b_shift_l」の右オペランドだけを(10進数で)1づつ増加させるような表記ができます。
おそらく人類の多くは1bitずつずれていくような16進数の変化よりも10進数で1ずつ増加していくような変化の方がなじみがあるわけで、 なじみがある方がコーディングミスする確率は減らせることを鑑みると、このような表記にも一定の意味があるかもしれません。
const SDL_WINDOW_FULLSCREEN = 0x1 ^b_shift_l 0 /* 0x01 */
const SDL_WINDOW_OPENGL = 0x1 ^b_shift_l 1 /* 0x02 */
const SDL_WINDOW_SHOWN = 0x1 ^b_shift_l 2 /* 0x04 */
const SDL_WINDOW_HIDDEN = 0x1 ^b_shift_l 3 /* 0x08 */
const SDL_WINDOW_BORDERLESS = 0x1 ^b_shift_l 4 /* 0x10 */
const SDL_WINDOW_RESIZABLE = 0x1 ^b_shift_l 5 /* 0x20 */
上記の定数の値はそれぞれ「0x01」「0x02」「0x04」「0x08」「0x10」「0x20」となります。 勿論これをそのまま16進数のuint型リテラルとして記述しても全く構わないのですが、 左ビットシフトを使ってこれを表現した場合、「^b_shift_l」の右オペランドだけを(10進数で)1づつ増加させるような表記ができます。
おそらく人類の多くは1bitずつずれていくような16進数の変化よりも10進数で1ずつ増加していくような変化の方がなじみがあるわけで、 なじみがある方がコーディングミスする確率は減らせることを鑑みると、このような表記にも一定の意味があるかもしれません。
これらの演算子まで使わなければならない状況は、現代においてはそれなりに基盤的で内部的な処理を行いたい場合に限られるかと思います。
従ってこのチュートリアルではこれ以上掘り下げることはしませんが、
Rarakuの標準ライブラリであるRrkDateの実装で左ビットシフト、右ビットシフトを使っていますので、
これらが簡単な応用例になるかと思います。
ビットの二項演算子:排他的論理和
その他のビットの二項演算子として「Exclusive OR」(排他的論理和)があります。 これはやや発展的な内容であり、(現代的な)通常のプログラミングにおいては(ジャンルにもよりますが) 登場頻度はここまで紹介したビット演算よりもさらに下がるものですのでここでは紹介程度に留めておきます。
/* Exclusive OR */
varia u = 0x01 ^b_xor 0x03 /* u = 0x02 */
補足: Exclusive OR(排他的論理和)とは?
この演算子について無理にここで学ぶ必要はないように思いますが、
一応このチュートリアルでもざっくりとだけ解説しておきます
(詳しくはこれらの用語でGoogle検索してもらった方がよいでしょう)。
Exclusive ORとは2進数で対応する位を比べた時に、その両方が異なる場合に限り(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 1、
0 と 1 では 1、
1 と 1 では 0
となります。
意義がよくわからない演算と思いますが、例えば暗号化処理などで利用されることがあります (これはExclusive ORを同じオペランドで二度繰り返すと元に戻る性質があるからです)。
Close
Exclusive ORとは2進数で対応する位を比べた時に、その両方が異なる場合に限り(対応する位を)1とするような演算です。
つまり
0 と 0 では 0、
1 と 0 では 1、
0 と 1 では 1、
1 と 1 では 0
となります。
意義がよくわからない演算と思いますが、例えば暗号化処理などで利用されることがあります (これはExclusive ORを同じオペランドで二度繰り返すと元に戻る性質があるからです)。
Close
ビットの単項演算子
Rarakuのビット演算子のうち、単項演算子(オペランドを一つとる演算子)をまとめたものを以下に示します。
varia u uint
/* 32bit整数否定 */
u = ^b_not32 0x02 /* u = 0xfffffffd */
/* 64bit整数否定 */
u = ^b_not64 0x02 /* u = 0xfffffffffffffffd */
ご覧の通り、ビットの単項演算子はビット否定演算子のみとなります。 ただしRarakuではこれは二つあり、それぞれ32bit整数否定と64bit整数否定となります。
上記でオペランドとして同じ「0x02」を与えていますが、 「^b_not32」ではそれを32bit整数とみなして否定をとるため結果は「0xfffffffd」となり、 一方「^b_not64」ではそれを64bit整数とみなして否定をとるため結果は「0xfffffffffffffffd」となります。
補足: ビット否定とは?
ビット否定についても念のため補足しておきます(ご存知の方はこれは読み飛ばしてください)。
上記の例では「0x02」と書いてありますが、「^b_not32」ではこれを32bit(4byte)とみなして「0x00000002」と考えます。 これを2進数で書くと長くなりますが「0000 0000 0000 0000 0000 0000 0000 0010」となります。 ビット否定とは2進数で対応する位の1と0を逆転させるような演算です。
つまり
0 では 1、
1 では 0、
となります。
従って「0000 0000 0000 0000 0000 0000 0000 0010」のビット否定をとれば結果は 「1111 1111 1111 1111 1111 1111 1111 1101」となります。 さらにこの結果を16進数で書けば「0xfffffffd」となります。
「^b_not64」ではこれを64bit(8byte)とみなして「0x0000000000000002」と考えます。 これも同様な演算になりますが、32bit版との違いは上位ビットに「0」が追加される形になることです。 従ってビット否定をとればそれらの上位ビットは「1」に変わりますから、結果を16進数で書けば「0xfffffffffffffffd」となります。
Close
上記の例では「0x02」と書いてありますが、「^b_not32」ではこれを32bit(4byte)とみなして「0x00000002」と考えます。 これを2進数で書くと長くなりますが「0000 0000 0000 0000 0000 0000 0000 0010」となります。 ビット否定とは2進数で対応する位の1と0を逆転させるような演算です。
つまり
0 では 1、
1 では 0、
となります。
従って「0000 0000 0000 0000 0000 0000 0000 0010」のビット否定をとれば結果は 「1111 1111 1111 1111 1111 1111 1111 1101」となります。 さらにこの結果を16進数で書けば「0xfffffffd」となります。
「^b_not64」ではこれを64bit(8byte)とみなして「0x0000000000000002」と考えます。 これも同様な演算になりますが、32bit版との違いは上位ビットに「0」が追加される形になることです。 従ってビット否定をとればそれらの上位ビットは「1」に変わりますから、結果を16進数で書けば「0xfffffffffffffffd」となります。
Close
ビットの複合代入演算子
復習になりますが算術演算子では「a += b」などと書くと「a + b」の計算結果が「a」に代入されるのでした。 またこのような計算結果が最終的に左辺に代入される演算子を複合代入演算子と呼ぶのでした。
Rarakuのビット演算でもこのような複合代入演算子が存在します。 これをまとめたものを以下に示します。
varia flag uint = 0
/* ビットセット(左側で与えたオペランド(整数変数)に対し、右側のオペランドで与えたビットを1にする) */
flag = 0x10
flag ^b_set 0x1 /* flag = 0x11 */
/* ビットアンセット(左側で与えたオペランド(整数変数)に対し、右側のオペランドで与えたビットだけを0とし、その他はそのまま残す) */
flag = 0x11
flag ^b_unset 0x10 /* flag = 0x01 */
/* ビットフィルタリング(左側で与えたオペランド(整数変数)に対し、右側のオペランドで与えたビットだけを1として残し、その他を0にする) */
flag = 0x11
flag ^b_filt 0x10 /* flag = 0x10 */
/* 左と右オペランドのExclusive ORを計算してから左オペランドへ代入 */
flag = 0x11
flag ^b_set_xor 0x10 /* flag = 0x01 */
/* 左ビットシフトを行ったものを左オペランドへ代入 */
flag = 0x21
flag ^b_set_shift_l 2 /* flag = 0x84 */
/* 右ビットシフトを行ったものを左オペランドへ代入 */
flag = 0x84
flag ^b_set_shift_r 2 /* flag = 0x21 */
最初の例では「^b_set」を使っており、これは左と右オペランドのビット和を計算しその結果を左オペランドへ代入します。 これは左オペランドを一種のフラグとみた場合、右オペランドで与えられたビットのフラグをONにするような処理になります。
次の例では「^b_unset」を使っており、これは「^b_set」とはちょうど逆の演算と見ることもできます。 つまり左オペランドを一種のフラグとみた場合、右オペランドで与えられたビットのフラグをOFFにするような処理になります。
次の例では「^b_filt」を使っており、これは左と右オペランドのビット積を計算しその結果を左オペランドへ代入します。 つまり左オペランドを一種のフラグとみた場合、右オペランドで与えられたビットのフラグだけをONとして残す(それ以外はOFFとする)ような処理になります。
「^b_filt」で与えるオペランドのビット否定をとれば結局「^b_unset」と同じ処理に帰結します。
つまり以下の二つは結果は同じになります。
Rarakuで「^b_unset」が用意されている理由は「^b_filt」だと長くなるこの処理を短く書くためです。
varia flag uint = 0
flag = 0x11
flag ^b_unset 0x10 /* flag = 0x01 */
flag = 0x11
flag ^b_filt (^b_not64 0x10) /* flag = 0x01 */
Rarakuで「^b_unset」が用意されている理由は「^b_filt」だと長くなるこの処理を短く書くためです。
残りの「^b_set_xor」「^b_set_shift_l」「^b_set_shift_r」についても同様です。 例えば「^b_set_xor」の場合、まず左と右オペランドのExclusive ORを一旦計算し、その計算結果を左オペランドへ代入するといった処理を 一括して行うものになります。
ビット演算子の優先順位
Rarakuではビットの二項演算子の優先順位はべき乗の算術演算子「^^」と同じです。 ですからビットの二項演算子のオペランドそれ自体が「なんらかの算術演算子を使った式」である場合、 そのオペランドを括弧で囲う必要がありますし、 逆に算術演算子(「^^」を除く)のオペランドに「ビット演算子の式」を指定する場合は括弧は必須ではありません。 例えば以下の例では、ビット演算「0x1 ^b_shift_l 2」の部分が優先的に評価されてこれがまず4になりますので、 全体としては「1 + 4 + 1」で結果は6と表示されます。
varia ans = 1 + 0x1 ^b_shift_l 2 + 1
Rrk_print( \=ans\n )
また以下の例では、括弧で括った「2 + 1」の部分が優先的に評価されてこれがまず3になり、 次にビット演算「0x1 ^b_shift_l 3」が評価されてこれが8となり、 全体としては「1 + 8」で結果は9と表示されます。
varia ans = 1 + 0x1 ^b_shift_l ( 2 + 1 )
Rrk_print( \=ans\n )
ただこのようなわかりにくいルールを覚えるくらいならやはり括弧を使いましょう。 式内での構造がわかりにくい式については、常にその構造を明示するように括弧で囲い、読み手にわかりやすくしておくべきです。 例えば最初の例でも、頑張って優先順位を調べるか思い出すかすれば括弧は不要とわかりますが、 それよりも常に次のようにしておけば悩む必要もなくなります。
varia ans = 1 + ( 0x1 ^b_shift_l 2 ) + 1
Rrk_print( \=ans\n )
Rarakuではビットの単項演算子(これはビット否定演算子のみですが)の優先順位は論理否定の演算子「!」と同じです。 これはビットの二項演算子よりも優先順位が高いものとなります。 ただくどいようですが、このようなわかりにくいルールを覚えるくらいなら括弧を使いましょう。
Rarakuではビットの複合代入演算子を使った場合、これは完全な文となります。 よってそもそも式中にこれを埋め込むことはできませんので、この場合は優先順位を考える必要そのものがありません。
ビット演算子のまとめ
Rarakuにおけるビット演算子はC言語などで提供されているものと機能は同じですが、表記はかなり異なります。 Rarakuではすべてのビット演算子は「^b_」で始まり、その後にその働きを示す識別子が続く形になります。 ただしビット和だけは「|」を使うこともできます(これはC言語と同じです)。
ビット演算子の優先順位はわかりにくいので、式中で使う場合は括弧で優先順位を明確化しておきましょう。
目次に戻る
tight文とimmut文とrefer文
tight文の基本
tight文はvaria文と同様、新しい変数を宣言するための文です。 「配列とモディファイア」のセクションでtightモディファイアというものを既に紹介しましたが、 これはそれが一つの文として独立したような働きをします。
tight文は、「tight 変数名 型(= 値)」という書式で指定します。 tight文は変数を宣言するという意味ではvaria文と同じです。 varia文と同じく初期化はしてもしなくても構いませんし、その識別子自身への再代入も許可されます。
tight文で宣言した変数が配列の場合、それはtightな配列となります。 すなわち要素の値の変更は許可されますが、要素数の変更は禁止されます。 この性質を要素数一定(constant number of elements)と呼びます。
構造体変数をtight文で宣言し、かつその構造体のメンバに配列を含む場合は、メンバとなる配列がtight指定されているかのような挙動になります
(ただし詳しくは「ダブルモディファイア」のセクションで述べますが、この挙動は調整することができます)。
またtight文で配列や構造体以外の型(例えば単なるint型)の変数を宣言した場合、
それは制限としてはvaria文と変わらないことになります。
配列の要素への値の代入や、他の非constな配列を右辺に与えることは許可されます。 例えば以下の通りです。
tight t int[] = { 1, 2 }
t[0] = 9 /* OK */
//Rrk_push_bk( t, 3 ) /* compile error */
varia v int[]
tight u int[]
Rrk_push_bk( v, 1 )
Rrk_push_bk( v, 2 )
u = v /* OK */
//Rrk_push_bk( u, 3 ) /* compile error */
このようにほとんどvaria文と同じように使えることがわかると思います。 ただしtight配列ではRrk_push_bkで要素数を変更するようなことは許可されません。 もしもそのような記述をした場合はコンパイルエラーとなります。
もう一つ例をみましょう。 以下をご覧下さい。
function func() tight int[] {
varia a int[3]
return a /* OK */
}
tight b = func() /* OK: tight文により新しいtight配列 b を宣言し、funcの戻り値で初期化 */
上記ではfuncの戻り値の型はtight配列であり、それを(tight文の)bの初期値として与えています。 この b は型推論により自動的にint[]型配列(可変長)となります。 この b を、仮に(tight文ではなくvaria文等で)固定長配列として明示的に宣言した場合、 要素数の関係性がマッチせず関数funcの戻り値を受けることはできません。 このケースでは「要素数が変更不可、要素の値は変更可能」な可変長tight配列として funcの戻り値を受け取る必要があり、そのためtight文の使用が必要となります。
このfuncの戻り値はtight文ではなくconst文で受け取ることもできます。 その後に b の要素の値の変更の可能性がなければconst文で受けた方がより頑強で望ましいでしょう。
後のセクションで登場するfinal文を使えば、一度varia文で宣言した(要素数可変な)可変長配列を
途中からtightな配列にすることもできます。
これについてはfinal文のセクションで詳しく述べます。
immut文の基本
immut文はvaria文と同様、新しい変数を宣言するための文です。 immut文は変数を宣言するという意味ではvaria文と同じです。 varia文と同じく初期化はしてもしなくても構いませんし、その識別子自身への再代入も許可されます。
immut文は、「immut 変数名 型( = 値)」という書式で指定します。
immut文はconst文と同じく、要素の変更が許可されない配列や構造体を宣言するために使用します。 この性質を集成体不変(aggregate immutability)と呼びます。 またconst文やtight文と同じく、宣言された変数が配列の場合、その要素数の変更が認められません(つまり「要素数一定」です)。
const文では初期化が必須で再代入が禁止されましたが、一方でimmut文ではその制限が撤廃されていることになります。 この性質はconstモディファイアとはまた別の性質(少し制限を緩めた性質)であるため、これをimmutモディファイアと呼ぶこともあります。
構造体変数をimmut文で宣言し、かつその構造体のメンバに配列を含む場合は、メンバとなる配列がimmut指定されているかのような挙動になります
(ただし詳しくは「ダブルモディファイア」のセクションで述べますが、この挙動は調整することができます)。
またimmut文で配列や構造体以外の型(例えば単なるint型)の変数を宣言した場合、
それは制限としてはvaria文と変わらないことになります。
immutで宣言された変数では、任意の文で宣言された変数/定数やリテラルを右辺に与えることが許可されます。 例えば以下の通りです。
const c int[] = { 1, 2 }
immut i int[] = c
immut j int[]
j = i /* OK */
j = c /* OK */
//j[0] = 9 /* compile error */
//Rrk_push_bk( j, 3 ) /* compile error */
このように宣言部についてはほとんどvaria文と同じように使えます。 ただしimmut配列やimmut構造体では「集成体不変」が規定されます(つまりその要素やメンバへの再代入が許可されません)。 例えば上記で「j[0] = 9」は配列要素への再代入となるためコンパイルエラーとなります。 またRrk_push_bkで要素数を変更するようなことも同様に許可されません。
もう一つ例をみましょう。 以下をご覧下さい。
function func() tight int[] {
varia a int[3]
return a /* OK */
}
immut b = func() /* OK: immut文により新しいimmut配列 b を宣言し、funcの戻り値で初期化 */
// b[0] = 9 /* compile error */
上記ではfuncの戻り値の型はtight配列であり、それを(immut文の)bの初期値として与えています。 この b は型推論により自動的にint[]型配列(可変長)となります。 またtightからimmutへの初期化したため、 配列bについては要素数一定はもちろん、集成体不変も規定されます。
refer文の基本
Rarakuでは参照変数(reference)という概念があります。 関数の引数においてreferモディファイアを指定すると、その引数の実体そのものを直接更新できましたが、あれも参照変数の一種です。 関数の引数以外の場所においても、refer文を使ってこの参照変数を宣言できます。
refer文は、「refer 参照変数名 型 = 参照される変数」という書式で指定します (tigit文やconst文と同様の書式です)。 refer文では必ず右辺を明示的に指定しなければなりません(省略は不可です)。 例えば次のようになります。
varia i = 0
refer r int = i /* 変数 i を参照する参照変数 r の宣言 */
r = 2
Rrk_print( \=i\n ) /* i の値は 2 となる */
上の例の2行目がrefer文で、最初の行で宣言した変数 i に対する参照変数を宣言しています。 このrefer文での「=」は、通常の意味での「=」とは違うことに注意しましょう。 この「=」は「r は i を指し示す」というよりむしろ「 r は i と同一である」とでも言うべき意味になり、 rへの再代入が発生する場合にこの違いが顕著に現れます。
3行目(「r = 2」)における「=」については、通常の意味での「=」と同じく再代入を意味するものになるのですが、 しかしこのとき実際の代入先は r が参照している変数 i となります (つまりこの代入により、i の値が 2 になります)。 実際、最後の行で i の値を表示させると確かに 2 となっていることが確認できます。
C++では型名に&を付けて参照変数を宣言できますが、
Rarakuの参照変数もあれとほとんど同じ概念です。
また、インクリメントやデクリメントに関しても同様に参照先が更新されます。 例えば以下の通りです。
varia i = 0
refer r = i
++r;
Rrk_print( \=i\n ) /* i の値は 1 となる */
--r;
Rrk_print( \=i\n ) /* i の値は 0 となる */
単独の変数を右辺に与える場合は、型推論されるため、refer文における型指定を省略できます。 上の例ではrefer文の型を省略していますが、右辺が単独の変数 i であるため、r の型は自動的に int 型となります。 この例の実行結果は以下のようになります。
i=1
i=0
このようにrefer文で宣言した参照変数は特殊なものですが、一方で普通の変数のように演算したり、それを確認表示することも可能です。 例えば以下の通りです。
varia i = 5
varia j = 10
refer r = i
refer q = j
r = r + q
Rrk_print( \=r\n ) /* r の値は 15 となる */
Rrk_print( \=i\n ) /* i の値も 15 となる */
if r > q {
Rrk_print( "OK.\n" )
} else {
Rrk_print( "NG.\n" )
}
上の例の実行結果は以下のようになります。
r=15
i=15
OK.
refer文の右辺で指定可能なもの
refer文はconst文と同様の書式であると述べましたが、refer文の右辺に指定可能な式には制限があります。 まずrefer文の右辺に指定できるのは基本的には(const文以外の文で宣言された)再代入可能な変数だけです。 const文で宣言された定数やリテラルを指定することはできません。 例えば以下はコンパイルエラーとなります。
const c = 2
refer r1 int = c /* コンパイルエラー : refer文の右辺としてconst宣言された定数を指定している */
refer r2 int = 2 /* コンパイルエラー : refer文の右辺として数値リテラルを指定している */
念のために補足しますが、varia文で宣言されたconststr型の変数を参照するのは問題ありません。 例えば以下は問題ありません。
varia cs conststr = "hello"
refer r = cs /* OK : csはvaria文で宣言された変数 */
conststrは(constという文字列が含まれてはいますが)再代入不可能な型というわけではなく、 あくまでその中味の文字列を変更することができない型になります。 従って(それがvaria/refer文で宣言されたものであれば)refer文で参照することが可能です。
以下のようにrefer文で宣言されたものをさらにrefer文の右辺に指定するのは問題ありません。
varia i = 10
refer r1 = i
refer r2 = r1 /* refer文で宣言されたr1を指定 */
この場合、r2が参照しているのは結局大元の変数 i となります。
その他、変数であれば後述するrefer型の戻り値を持つ関数呼び出し、配列の要素指定、構造体のメンバ指定が単独で現れる場合であれば指定が可能です。 例えば以下は問題ありません。
struct MyStruct {
i int
}
varia my MyStruct = {}
refer r = my.i /* OK: myはvariaで宣言された変数であり、そのメンバを指定している */
一方、それらが複数組み合わさった(例えば二項演算子などを用いた)演算式である場合はコンパイルエラーとなります。 例えば以下は(割と単純な式ではありますが)コンパイルエラーとなります (参照先がiとjのどちらであるか判断が付かないからです)。
varia i = 1
varia j = 2
refer r int = i + j /* コンパイルエラー : refer文の右辺として複数の変数の演算式を指定している */
ただし三項演算子については「:」の両側にある式がどちらもrefer文で指定可能なものである場合のみ、特別に指定が許可されます。 例えば以下のようになります。
varia ary int[3] = [ 1, 1, 1 ]
varia i = 10
refer r = i > 0 ? i : ary[0] /* OK: 三項演算子の値となる二つの式はどちらもrefer文の右辺として指定可能 */
++r;
Rrk_print( \=i \n )
refer q = i == 0 ? i : ary[0] /* OK: 三項演算子の値となる二つの式はどちらもrefer文の右辺として指定可能 */
++q;
Rrk_print( "ary[0]=" ary[0] \n )
上の例では三項演算子内で指定された「i」と「ary[0]」はどちらもrefer文の右辺として指定可能な式なので、 三項演算子全体としてもrefer文の右辺に指定可能となります(実際にどちらを参照するかは動的に決まることになります)。 この例の実行結果は以下のようになります。
i=11
ary[0]=2
refer文の一括宣言
const/varia/tigit/immut文では「,」で区切ることによって一つの文で複数の定数/変数を宣言できますが、 refer文でも同様に「,」区切りで指定ができます。 ただしrefer文の場合はすべての指定に必ず明示的な右辺値が必要です。 例えば以下のようになります。
varia a=1, b=2, c=3
refer p=a, q=b, r=c
必ず明示的な右辺値が必要ということは、refer文では以下のような記述はできないことになります。
varia c=3
refer p, q, r=c /* コンパイルエラー: pとqが明示的に初期化されていない */
もし、pとqとrすべてに同じcを参照させたいならば、以下のように記述しなればなりません。
varia c=3
refer p=c, q=c, r=c /* OK */
const/varia/tigit/immut文の時と同様、最後の指定の後にも「,」を入れることができます。ただしその場合は文末の「;」が必須となります。 またこれを使って途中改行して記述することも可能です。
varia
a=1,
b=2,
c=3,
;
refer
p=a,
q=b,
r=c,
;
もっとも、refer文が登場するというのはそれほど頻発するようなことではなく、
しかもそれが登場せざるを得ないというのはそれなりに特殊な出来事であるように思いますので、
(このような簡易記法を使わず)目立つようにすべての参照変数にreferを付与して書く方がよいのかもしれません。
関数の戻り値と左辺値
Rarakuでは関数の戻り値が通常の型の場合、それを代入文の左辺として直接記述することはできません。 例えば以下はコンパイルエラーとなります。
function func() int
{
varia i = 1
return i
}
func() = 2 /* コンパイルエラー */
しかし、戻り値の型にreferを指定した場合はこれが許可されます。 例えば以下はOKです。
function func() refer int
{
varia i = 1
return i
}
func() = 2 /* OK */
Rarakuでは通常は関数を終了した時点で内部のローカル変数(上の例ではi)も破棄されます。 しかし戻り値にreferを指定した場合、それが参照するローカル変数については例外的にその破棄が少し延期されます(これを自由変数化と呼びます)。 上の例の場合、「func() = 2」という文を実行する時点ではまだ、func関数内のローカル変数 i の寿命は破棄されておらず、 右辺値 2 を(ローカル変数iを指し示している)左辺値へ代入することができます。 「func() = 2」という文が終了した時点でローカル変数 i を指し示すものが何もないので、この時点で初めて i が破棄されます。
ローカル変数の寿命がさらに長く持続する例も見てみます。 以下の例をご覧ください。
function func() refer int
{
varia i = 1
return i
}
refer r = func() /* 戻り値をrが参照 */
/* rで参照しているため、これ以降もローカル変数 i の寿命は続く */
r = 2 /* ローカル変数 i に 2 を代入 */
前回の例とは異なり、今回は「refer r = func()」において関数funcの戻り値をrefer文で参照しています。 このとき、func内部のローカル変数 i は r によって改めて参照され直されるため、これ文以降もその寿命は続くことになります。 r の寿命が終了した時点で、ローカル変数 i の寿命も終了します。
因みに戻り値の型がrefer指定されていない関数呼び出しをrefer文の右辺とした場合はコンパイルエラーとなります。 例えば以下をご覧ください。
function func() int
{
varia i = 1
return i
}
refer r = func() /* コンパイルエラー: 戻り値の型がreferではない関数をrefer文の右辺に指定している */
上記の関数funcは(refer指定のない)単なるintを返すものですが、これをrefer文の右辺に指定しているため、コンパイルエラーとなります。
このようにreferを指定しなかった場合、Rarakuは戻り値用の一時変数を(ローカル変数とは別に)内部的に別途割り当てますが、
外部のrefer文においてそのような一時変数を参照しても実質的に意味がありません。
そのため、それをrefer文の右辺として指定するような記述はコンパイルエラーとなる仕様になっています。
関数内の完全なローカル変数を参照するのではなく、関数の引数由来の戻り値を参照することもできます。 以下の例をご覧ください。
function func( refer i int ) refer int
{
return i
}
varia i = 0
refer r = func( i ) /* 戻り値をrが参照 */
r = 2 /* これは結局 i に 2 を代入しているのと同じ */
Rrk_print( \=i\n ) /* iの値を確認 */
上の例では、refer型の引数としてiを与え、さらにそれをrefer型の戻り値として返し、 refer文によってrがそれを参照しています。 この場合、r は結局大元の i を参照することになり、「r = 2」という文は i へ 2 を代入することと同じになります。 この例の実行結果は以下のようになります。
i=2
ここまでの例ですとなぜわざわざこんなことをするのかよくわからないと思いますが、
その意味については後でもう少し実用的な例を見て考えることにし、ここでは一旦置いておきましょう
(特にこの参照変数の分野に関しては、実用的な例になればなるほどコードの難易度が割と急激に上がってしまう傾向があるように思います)。
配列とそのインデックスによるアクセスが絡む場合でも同様です。 以下の例をご覧ください。
function refElem( ary int[], idx int ) refer int
{
varia dummy = 0 /* 参照として返すため敢えて変数として宣言 */
return idx < 0 || idx >= Rrk_arynum(ary) ?
dummy : /* ダミーへの参照を返す */
ary[ idx ] /* 要素の参照を返す */
}
varia ary int[3]
refElem( ary, 0 ) = 10
refElem( ary, 1 ) = 20
refElem( ary, 2 ) = 30
refElem( ary, 3 ) = 40 /* 配列境界を超える場合は替わりにダミー変数dummyに代入させる */
Rrk_print( "ary[0]=" ary[0] \n )
Rrk_print( "ary[1]=" ary[1] \n )
Rrk_print( "ary[2]=" ary[2] \n )
上の例で、refElemはインデックス(idx)が適切な範囲にある場合のみ配列(ary)のidx番目の要素への参照を返しています。 それ以外の場合はダミー変数dummyへの参照を返します。 このように三項演算子が絡んでいても問題ありません。 「refElem( ary, 0 ) = 10」という文では、配列aryの0番目の要素へ値10を設定できることになりますし、 「refElem( ary, 3 ) = 40」という文では、ダミー変数dummyへ値40を設定する(つまり配列aryには何の影響も与えない)ことになります。
ここでは説明のためこのような例を挙げましたが、
これはあまり実用性のない例だとは思います。
実際には多分「setElem( varia ary int[], idx int, val int ) bool」のようなsetter関数を作る方が望ましいと思います。
この例の実行結果は以下のようになります。
ary[0]=10
ary[1]=20
ary[2]=30
少しだけ実用度を上げた例(二分木を扱う例)も見てみましょう。 ただし少し難易度は上がってしまいますので頑張ってください (よくわからない場合、ここは後回しにしても構わないと思います)。
補足: 二分木とは?
二分木についても念のため補足しておきます(ご存知の方は読み飛ばしてください)。
まず喩え話からしましょう。性別のない生命体を考えます。 宇宙で最初に現れたその一体の生命体を「ルート」と名づけましょう。
この生命体はそれ一体だけで自分の子を作ることができます(セルとセルジュニアみたいなものです(ナメッ●星人の方がいいですかね?))。 その子もまた(それ一体だけで)自分の子(最初の生命体から見た場合孫)を作ることができます。 中には自分の子を一切作らない者もいるかもしれませんし、沢山作る者もいるかもしれませんが、 そのようにして「ルート」を先祖とした一つの家系ができあがります。
これを情報科学(computer science)では「木(tree)」と呼びます。 また個々の「生命体」のことを「ノード(node)」と呼びます。 なぜこれが「木」という名前になっているかと言うと、この家系図を上下逆転し、「ルート(root)」を「根」とみなしたとき まるで木のように上へ末広がって見えることに由来します。
今この生命体に最大でも二体までしか自分の子を作れないという制限を神様が与えたとしましょう。 その場合、この木は特に「二分木」と呼ばれます。
さて、ここまでは変な喩え話でしたが、ここから先が本題になります。 その二分木をどうやってプログラミングで表現するかを考える必要があります。 まず「木」とは言いますが、結局のところその構成要素はすべて「ノード(node)」であることがポイントです (ルートですらノードの一種になります)。 つまりノードさえうまく表現できれば木全体も表現できることになります。
ではそのノードをコード上でどう表現しましょうか? そのためには以下のような自己参照構造体を使います。
メンバleft_とright_はこのノードの子(への参照)を表しています。 今、二分木を考えていますから子の数は最大でも二つあれば十分です。 それぞれの子をleft_(左の子)、right_(右の子)と表現するわけです。 子が一人の場合はどちらか一方をnullにするものとしましょう。 子が一人もいない場合は両方ともnullにするものとしましょう。
left_とright_のいずれにもデフォルトメンバ初期化子として「=null」が指定されていますので、 デフォルトでは子が一人もいない状態となります。 つまり、以下のように「st_root」というルートを宣言した場合、まだ子は一人もいない状態であるはずです。
st_rootの子を一つ生成したい場合、例えば以下のようになるでしょう。
st_rootの子の子を一つ生成したい場合(つまり孫を生成したい場合)、以下のようになります。
このように左辺に生成先の子を指定し、右辺には「{}」などを指定すればよいわけです。 最後のvaria文ではst_root.left_の値を p へ代入しています。 そのため「p.left_ = { }」とすればそれは「st_root.left_.left_ = { }」と記述したのと同じになります。 ただしvaria文の場合、左辺は構造体のメンバにアクセスする形となっていなければなりません。 「p = { }」のように p それ自体へ代入してしまう形ですと、元のst_rootとの関係性が完全に上書き消失してしまいうまくいきません。
一方、refer文の場合であれば、この関係性の消失を防ぐことができます。 例えば以下の通りです。
最後のrefer文ではst_root.right_そのものを参照変数 r で参照しています。 そのため単に「r = { }」とすればそれは「st_root.right_ = { }」と記述したのと同じになります。
残りのメンバkey_は文字列型となっています。 これは二分木という構造上の観点からは別に必須ではありませんが、 それぞれのノードをどのように格納するかを決定するために用意されたものです。 これだけですと何を言ってるかよくわからないと思いますが、とりあえずはノードを一意に識別する識別子のようなものだと考えましょう。 同じkey値を持つノードは二つは存在しないということです。
そして例えば、"bbb"というキー値を持つノードがあったとき、 その子のノードのキー値が"aaa"のように(辞書順で見たとき)"bbb"より前に位置する文字列であった場合、その子は左の子とする といった格納のルールを設けることにしましょう (逆に子ノードのキー値が"ccc"のように(辞書順で見たとき)"bbb"より後に位置する文字列であった場合、その子は右の子とします)。
単に子を最大二個持つという構造上の性質に加え、このような格納上のルールを追加することによって それぞれのノードを探索する効率を上げることができます。
Close
まず喩え話からしましょう。性別のない生命体を考えます。 宇宙で最初に現れたその一体の生命体を「ルート」と名づけましょう。
この生命体はそれ一体だけで自分の子を作ることができます(セルとセルジュニアみたいなものです(ナメッ●星人の方がいいですかね?))。 その子もまた(それ一体だけで)自分の子(最初の生命体から見た場合孫)を作ることができます。 中には自分の子を一切作らない者もいるかもしれませんし、沢山作る者もいるかもしれませんが、 そのようにして「ルート」を先祖とした一つの家系ができあがります。
これを情報科学(computer science)では「木(tree)」と呼びます。 また個々の「生命体」のことを「ノード(node)」と呼びます。 なぜこれが「木」という名前になっているかと言うと、この家系図を上下逆転し、「ルート(root)」を「根」とみなしたとき まるで木のように上へ末広がって見えることに由来します。
ここでは正確な定義はとりあえず置いておきましょう。
「木」について全く初めてならとりあえずはこの程度の理解でも十分です。
今この生命体に最大でも二体までしか自分の子を作れないという制限を神様が与えたとしましょう。 その場合、この木は特に「二分木」と呼ばれます。
さて、ここまでは変な喩え話でしたが、ここから先が本題になります。 その二分木をどうやってプログラミングで表現するかを考える必要があります。 まず「木」とは言いますが、結局のところその構成要素はすべて「ノード(node)」であることがポイントです (ルートですらノードの一種になります)。 つまりノードさえうまく表現できれば木全体も表現できることになります。
ではそのノードをコード上でどう表現しましょうか? そのためには以下のような自己参照構造体を使います。
struct Node {
left_ Node^?=null
right_ Node^?=null
key_ = ""
}
メンバleft_とright_はこのノードの子(への参照)を表しています。 今、二分木を考えていますから子の数は最大でも二つあれば十分です。 それぞれの子をleft_(左の子)、right_(右の子)と表現するわけです。 子が一人の場合はどちらか一方をnullにするものとしましょう。 子が一人もいない場合は両方ともnullにするものとしましょう。
left_とright_のいずれにもデフォルトメンバ初期化子として「=null」が指定されていますので、 デフォルトでは子が一人もいない状態となります。 つまり、以下のように「st_root」というルートを宣言した場合、まだ子は一人もいない状態であるはずです。
struct Node {
left_ Node^?=null
right_ Node^?=null
key_ = ""
}
varia st_root Node /* ルート:子はまだいない */
st_rootの子を一つ生成したい場合、例えば以下のようになるでしょう。
struct Node {
left_ Node^?=null
right_ Node^?=null
key_ = ""
}
varia st_root Node /* ルート:子はまだいない */
st_root.left_ = {} /* st_rootの「左の子」を生成 */
st_rootの子の子を一つ生成したい場合(つまり孫を生成したい場合)、以下のようになります。
struct Node {
left_ Node^?=null
right_ Node^?=null
key_ = ""
}
varia st_root Node /* ルート:子はまだいない */
final notnull st_root.left_ = {} /* st_rootの「左の子」を生成 */
st_root.left_.right_ = {} /* st_rootの「左の子」の「右の子」を生成 */
varia p = st_root.left_ /* (長くなるので)一旦 p に格納 */
p.left_ = {} /* st_rootの「左の子」の「左の子」を生成 */
このように左辺に生成先の子を指定し、右辺には「{}」などを指定すればよいわけです。 最後のvaria文ではst_root.left_の値を p へ代入しています。 そのため「p.left_ = { }」とすればそれは「st_root.left_.left_ = { }」と記述したのと同じになります。 ただしvaria文の場合、左辺は構造体のメンバにアクセスする形となっていなければなりません。 「p = { }」のように p それ自体へ代入してしまう形ですと、元のst_rootとの関係性が完全に上書き消失してしまいうまくいきません。
一方、refer文の場合であれば、この関係性の消失を防ぐことができます。 例えば以下の通りです。
struct Node {
left_ Node^?=null
right_ Node^?=null
key_ = ""
}
varia st_root Node /* ルート:子はまだいない */
refer r = st_root.right_ /* (長くなるので)一旦 r で参照 */
r = {} /* st_rootの「右の子」を生成 */
最後のrefer文ではst_root.right_そのものを参照変数 r で参照しています。 そのため単に「r = { }」とすればそれは「st_root.right_ = { }」と記述したのと同じになります。
残りのメンバkey_は文字列型となっています。 これは二分木という構造上の観点からは別に必須ではありませんが、 それぞれのノードをどのように格納するかを決定するために用意されたものです。 これだけですと何を言ってるかよくわからないと思いますが、とりあえずはノードを一意に識別する識別子のようなものだと考えましょう。 同じkey値を持つノードは二つは存在しないということです。
そして例えば、"bbb"というキー値を持つノードがあったとき、 その子のノードのキー値が"aaa"のように(辞書順で見たとき)"bbb"より前に位置する文字列であった場合、その子は左の子とする といった格納のルールを設けることにしましょう (逆に子ノードのキー値が"ccc"のように(辞書順で見たとき)"bbb"より後に位置する文字列であった場合、その子は右の子とします)。
単に子を最大二個持つという構造上の性質に加え、このような格納上のルールを追加することによって それぞれのノードを探索する効率を上げることができます。
Close
以下の例は、二分木のルートとなるノード(st_root)にinsert関数で新しいノードを追加するといったものになります。
struct Node {
left_ Node^?
right_ Node^?
key_ = ""
}
immut st_root Node^? = null
function findChild( key conststr, refer r Node^? ) refer Node^?
{
if r {
immut p Node = r
while p.key_ != key {
if key < p.key_ {
if notnull p.left_ && p.left_.key_ != key {
p = p.left_
}:{
/* new or already */
return p.left_
}
}:{
if notnull p.right_ && p.right_.key_ != key {
p = p.right_
}:{
/* new or already */
return p.right_
}
}
}
}
return r
}
function insert( key conststr ) Node^?
{
refer dst = findChild( key, st_root )
if dst { /* already exist */
Rrk_print( "key=[" key "] is already exist. skip\n" )
return null
}
varia new Node = { null, null, "" }
new.key_ = key->string
dst = new
return new
}
insert( "aaa" )
insert( "bbb" )
insert( "ccc" )
二分木に新しいノードを追加する場合、まずどの位置に追加するかを決めなければなりません。 その位置をfindChild関数によって求め、戻り値としてそれを返しています。 insert関数内では、findChild関数の戻り値を参照変数dstとして受け取り、 最終的に新しく確保したノード new を dst へ代入することによって ノードの新規登録を行っています。 つまりここで「dst = new」という代入文を行う必要があるため、 わざわざ dst を参照変数としているわけです。
findChildの中身についても考えましょう。 新しいノードを追加すべき位置は、与えたkeyの値や現在の木の状態によって変わります。 findChildでは、与えたkeyの値に応じてルートノードから順に(子の)ノードを探索していきます。
st_rootそれ自体がnullの場合(一番最初はそのはずですが)は、まずはそれが見つけ出すべきノードになりますので、 この場合は仮引数の r 自体(この r も参照変数ですから結局st_root自体を意味します)を参照させて返さなければなりません。 そのためまずは「if r」で全体を囲って切り分けています。 一方、st_rootがnullではない場合、key値を「<」で比較し(つまり文字列として辞書順で比較し)、 探索対象を左側の子ノードとするか右側の子ノードとするかを決めます。 このとき、その子ノードがnullとなっているならば、そのノードこそが new を代入すべき代入先となります。 つまりfindChildが見つけ出すべきノードですので、それを参照変数として返します。
ここでは return p.left_ や return p.right_ といったように
「構造体のメンバを参照先と見なした」参照変数を返すといったような処理を行っています。
このようにすることで、findChildの外からは、もはやleft_やright_だのといった違いを考慮する必要がなく、
さらにはそれへ代入するような処理が可能となります。
つまりこれは代入先の抽象化であり、参照変数を考える場合、この点は本質的に重要です。
上記のコードでは(ノードそれ自体がnullではなく)ノードのkey値が仮引数で与えたkey値とちょうど等しい場合も これを戻り値として返しています。 この場合、そのkeyに相当するノードは既に登録済みであることを意味します。 そのため、insert関数は何もせず "already exist. skip" といったメッセージを表示して終了します。
参考: 途中で別の変数へ参照し直すことはできるか?
参照変数はrefer文の初期化子によって一番最初にその参照先を決定します。
その後はずっと同じ参照先を参照したままであるわけですが、
この参照先を途中で変更することはできないでしょうか?
例えばループ文やif文など複雑な処理を経由してようやく参照先が決まるような状況は割と想定できます。 このようなとき、参照先を途中で変更できると便利ではないでしょうか?
しかし結論から言えばRarakuではこれはできません。
仮にこれを認める仕様にしたとして、また仮に「^rerefer」といった(架空の)演算子で参照先を途中で変更可能であるとしましょう。 しかしそれを許可してしまうと、今度は以下のような循環参照が生じる可能性を考慮しなくてはならなくなります。
上記の「r1 ^rerefer r2」によって、r1の参照先が途中でr2に変更されたとします。 この場合 r1 は r2 を参照していますし、r2 は r1 を参照しているという 結局参照している実体が何なのかよくわからない状況に陥ります。 強いて言えば、r2 は元々 r1 経由で i を参照していましたから、r1 も結局 i を参照している形に落ち着くような気もしますが、 一方で r1 の参照先が変わったので r2 の参照先も変化するはずだという解釈もできます。 いずれにせよ混乱の元にはなります。
上記は少し極端な例ですが、例えば以下のようにもう少し単純な状況でも解釈に混乱を生じるかもしれません。
上記の「r1 ^rerefer j」によって、r1の参照先が途中で j に変更されたとします。 この場合 r1 は最終的に j を参照していますが、 では r2 は何を参照していることになるのでしょう? この場合、r2 は元々 r1 経由で i を参照していましたから結局 i を参照している形に落ち着くような気もしますが、 一方で r1 の参照先が変わったので r2 の参照先も j であるべきではないかという解釈もできます。
コンパイル時の解析でこのような混乱が生じ得る状況を検出し、そのような状況については特別にコンパイルエラーで弾くといったことは 多分ある程度可能だとは思うのですが、Rarakuではそこまでの解析を行いません。
(ループ文やif文など)複雑な処理を経由してようやく参照先が決まるような状況でも、 それを一旦別関数にしてその戻り値をreferの初期化子として与えれば(若干工夫は必要になるかもしれませんが)対応は可能と思います。 上記の例で見たfindChildで行っている処理などはまさにこの例になります。 関数とした場合、その処理はrefer文とは離れた場所に記述することになりますが、 それを嫌うのであれば、後のセクションで述べる無名関数やローカル関数等を使う方法もあります。
Close
例えばループ文やif文など複雑な処理を経由してようやく参照先が決まるような状況は割と想定できます。 このようなとき、参照先を途中で変更できると便利ではないでしょうか?
しかし結論から言えばRarakuではこれはできません。
これは C++ の参照変数と同様です。
ただしC/C++ではポインタに別の変数のアドレスを再代入することによって
上記に該当するようなことはできます。
仮にこれを認める仕様にしたとして、また仮に「^rerefer」といった(架空の)演算子で参照先を途中で変更可能であるとしましょう。 しかしそれを許可してしまうと、今度は以下のような循環参照が生じる可能性を考慮しなくてはならなくなります。
varia i = 10
refer r1 = i
refer r2 = r1
r1 ^rerefer r2 /* 実際にはこんな演算子はRarakuに存在しないが仮に可能とした場合 */
上記の「r1 ^rerefer r2」によって、r1の参照先が途中でr2に変更されたとします。 この場合 r1 は r2 を参照していますし、r2 は r1 を参照しているという 結局参照している実体が何なのかよくわからない状況に陥ります。 強いて言えば、r2 は元々 r1 経由で i を参照していましたから、r1 も結局 i を参照している形に落ち着くような気もしますが、 一方で r1 の参照先が変わったので r2 の参照先も変化するはずだという解釈もできます。 いずれにせよ混乱の元にはなります。
上記は少し極端な例ですが、例えば以下のようにもう少し単純な状況でも解釈に混乱を生じるかもしれません。
varia i = 10
varia j = 20
refer r1 = i
refer r2 = r1
r1 ^rerefer j /* 実際にはこんな演算子はRarakuに存在しないが仮に可能とした場合 */
上記の「r1 ^rerefer j」によって、r1の参照先が途中で j に変更されたとします。 この場合 r1 は最終的に j を参照していますが、 では r2 は何を参照していることになるのでしょう? この場合、r2 は元々 r1 経由で i を参照していましたから結局 i を参照している形に落ち着くような気もしますが、 一方で r1 の参照先が変わったので r2 の参照先も j であるべきではないかという解釈もできます。
コンパイル時の解析でこのような混乱が生じ得る状況を検出し、そのような状況については特別にコンパイルエラーで弾くといったことは 多分ある程度可能だとは思うのですが、Rarakuではそこまでの解析を行いません。
上記のように単純なケースではまだよいですが、参照先が配列のインデックスによるアクセスや、自己参照構造体のメンバや、
関数の引数や戻り値(リカーシブコールが発生している可能性もあります)や、
三項演算子等を途中で仲介するような状況(何が参照されるかは動的に決まるような状況も含まれると考えられます)、
その他諸々を考慮すると、この種の問題を完全かつ正確に検出するのはかなり困難になるのではないかと思います。
(ループ文やif文など)複雑な処理を経由してようやく参照先が決まるような状況でも、 それを一旦別関数にしてその戻り値をreferの初期化子として与えれば(若干工夫は必要になるかもしれませんが)対応は可能と思います。 上記の例で見たfindChildで行っている処理などはまさにこの例になります。 関数とした場合、その処理はrefer文とは離れた場所に記述することになりますが、 それを嫌うのであれば、後のセクションで述べる無名関数やローカル関数等を使う方法もあります。
Close
refer文とnullable
通常、refer文においては右辺から左辺の型は推論可能なため、左辺の型を明記することはあまりないかもしれませんが、 もしも明記するならば、右辺がnullableの場合は左辺の型には必ず「^?」を付記し、 右辺がnotnullの場合は逆に「^?」を付記しないようにする必要があります。
通常のvaria文等では、右辺がnotnull、左辺がnullableといった指定も許されますが、
refer文ではこのような柔軟性はないということです。
また、あまりないことかもしれませんが、refer文を経由すると表面上はnotnull化された変数に 間接的にnullを代入できてしまう場合があります。 例えば以下をご覧下さい。
varia s string^? = ""
refer r = s
if s {
/* sをnotnull化 */
r = null /* r はnotnull化の範囲外であるため、この代入が可能 */
s[0] /* NG: Runtime error as null */
}
上記では、まずsをnullableとして宣言し、それをrefer文でrとして受けています。 その後、if文により s はnotnull化されるため、ifブロック内部においては s に直接nullを代入することはできませんが、 r はそのnotnull化の範囲外であるため、依然としてnullへの代入が可能となります。
しかしこの場合、実際には r は s を参照しており、またRarakuのnotnull化はrの参照先までは解析を行わないため、 結果的に ifブロック内部においてsへnullが代入される形になります。 そのため、最後の「s[0]」において、null配列に対する要素のアクセスが発生し、 nullに関するランタイムエラーが発生することになります。
もっとも、現実的にはわざわざこのようなコードを書くことはないと思いますが、 Rarakuにおけるnotnull化の限界点の一つとしてこのような状況が有り得ることは知っておいてもよいでしょう。
tight文とimmut文とrefer文のまとめ
tight文はtightモディファイアを一つの文として独立させたようなものになります。
refer文を使って宣言された識別子に代入処理やインクリメント等で値の更新を行うと、 それが参照している実体の変数そのものが実際に更新されます。 これにより代入処理において、その代入先を抽象化して記述することができます。
目次に戻る
ダブルモディファイア
はじめに
この「ダブルモディファイア」のセクションはかなり難解です。 そのため、よくわからないようであれば雰囲気だけを掴んで次のセクションに進んでも構いません。
集成体の初期化と再代入の規則について復習
まずこのセクションの本題に入る前に、配列と構造体の「初期化」と「再代入」の可否の規則(アサイナビリティ)について復習しましょう。 配列と構造体をまとめて集成体(aggregate)と呼びます。 そのすべての組み合わせと可否をまとめて表にしたものを以下に示します(配列と構造体のセクションで示した表にimmutとtightを付け加えたものです)。
- 初期化の場合
- 再代入の場合
| 左辺値 | 右辺値 | OK/NG |
|---|---|---|
| const | const | OK |
| const | immut | OK |
| const | tight | OK |
| const | varia | OK |
| immut | const | OK |
| immut | immut | OK |
| immut | tight | OK |
| immut | varia | OK |
| tight | const | NG |
| tight | immut | NG |
| tight | tight | OK |
| tight | varia | OK |
| varia | const | NG |
| varia | immut | NG |
| varia | tight | NG |
| varia | varia | OK |
| 左辺値 | 右辺値 | OK/NG |
|---|---|---|
| const | const | NG |
| const | immut | NG |
| const | tight | NG |
| const | varia | NG |
| immut | const | OK |
| immut | immut | OK |
| immut | tight | OK |
| immut | varia | OK |
| tight | const | NG |
| tight | immut | NG |
| tight | tight | OK |
| tight | varia | OK |
| varia | const | NG |
| varia | immut | NG |
| varia | tight | NG |
| varia | varia | OK |
尚、これは集成体(配列や構造体)の規則であって、非集成体(int型など)ではまた規則が変わってきます。
非集成体の場合、「初期化」についてはすべての組み合わせでOKであり、
また「再代入」については左辺値がconstの場合以外は全てOKとなります。
要素次数
多次元配列や何層にもネストされた構造体を用いた場合を考えましょう。 まず以下をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
const p InfoAry[] = [ [ {1} ] ]
上記では、p、p[0]、p[0][0]、p[0][0].m といったような形式のアクセスがあり得ます。 また次をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
const varia p Info = { { { 1 } } }
上記では、p、p.a、p.a.b、p.a.b.m といったような形式のアクセスがあり得ます。
Rarakuでは配列のインデックスアクセスや構造体のメンバアクセスの混在したチェーンが トップレベルから数えて何レベルあるかを示す指数を要素次数と呼んでいます。
これは一般的な用語ではありませんが、このセクションの説明全般においてこの用語を使用しますので、
ここで定義は押さえておく必要はあります。
例えば一つ前の例では、pを0次要素、p[0]を1次要素、p[0][0]を2次要素、p[0][0].mを3次要素と呼びます。 また同様に上記の例では、pを0次要素、p.aを1次要素、p.a.bを2次要素、p.a.b.mを3次要素と呼びます。
念のためもう一つ例を見ましょう。
struct SubB {
m int
}
struct SubA {
b SubB[] = [ {} ]
}
struct Info {
a SubA
}
const varia p Info = { { { 1 } } }
上記では、p、p.a、p.a.b、p.a.b[0]、p.a.b[0].m といったような形式のアクセスがあり得ます。 そしてこの場合、pを0次要素、p.aを1次要素、p.a.bを2次要素、p.a.b[0]を3次要素、p.a.b[0].mを4次要素と呼びます。
要素数一定/再代入禁止/集成体不変/受容性のモディファイア
ここでモディファイアconst/immut/tight/variaについてより厳密な区分をしましょう。
Xの要素数の変更が禁止されるとき、 これを要素数一定(constant number of elements)と呼びます。
また、「X=Y」といった形式の再代入が禁止されるとき、 これを再代入禁止(reassignment impossibility)と呼びます。
また、nを0以上の整数とし、n次要素X が集成体(配列や構造体)であり、 n+1次要素への再代入(例えば「X[0]=Y」といった形式の配列要素への再代入や、「X.m=Y」といった形式の構造体要素(メンバ)への再代入)が禁止されるとき、 これを集成体不変(aggregate immutability)と呼びます。
n次要素Xが集成体不変である場合、n+1次要素への再代入のみが禁止されると定義します。
このときn+2次要素以降の再代入については何も規定しません。
例えばXが集成体不変であっても、「X[0][0]=Y」や「X.m.n=Y」といった形式の再代入が禁止されるとは限りません。
次に受容性のモディファイアというものを考えます。
「Y=Z」という再代入文があり、 Zとして任意のものを指定可能であるとき、 これをimmut受容性(immut receptivity)と呼びます。
また、「Y=Z」という再代入文があり、 Yが集成体であるならばZとしてtight/varia型のみを指定可能であり、 Yが非集成体(int等)であるならばZとしてconst/immut/tight/variaのすべてが指定可能なとき、 これをtight受容性(tight receptivity)と呼びます。
また、「Y=Z」という再代入文があり、 Yが集成体であるならばZとしてvaria型のみを指定可能であり、 Yが非集成体(int等)であるならばZとしてconst/immut/tight/variaのすべてが指定可能なとき、 これをvaria受容性(varia receptivity)と呼びます。
例えばconst文で「const X = I」と書いた場合、このX は「再代入禁止」並びに「要素数一定」並びに「集成体不変」が規定されます。 また再代入は禁止ですので、「immut受容性」「tight受容性」「varia受容性」のいずれも規定されません。
これらは一般的な用語ではありませんが、このセクションの説明全般においてこの用語を使用しますので、
ここで定義は押さえておく必要はあります。
ここから先の説明では、これらの厳密な区分がしばしば重要となります。
const文のモディファイア
今まで普通に使用してきたconst文ですが、実はこれにもモディファイアを指定することができます。 これは「const モディファイア 識別子 型名」といった書式になります(ただし初期化は必須です)。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
const varia a Info[] = v /* OK */
//a = v /* compile error */
//Rrk_push_bk( a, c[0] ) /* compile error */
//a[0] = v[0] /* compile error. */
a[0].m = 9 /* OK. */
const varia d Info[] = [ v[0] ] /* OK */
//const varia e Info[] = [ c[0] ] /* compile error. */
上記の例ではconst文にvariaモディファイアを指定し、配列を宣言しています。
最初の「const」はconst文の開始を表すトークンではあるのですが、 形式上「const varia」とモディファイアとしてのトークンが二つ並ぶ表記ともなります。 Rarakuではこのことをダブルモディファイアと呼びます。 そしてこれらのうち一番目のモディファイアを第1モディファイア、二番目のモディファイアを第2モディファイアと呼称します。 第2モディファイアは例えば初期化において、右辺に構造体や配列の初期化子が与えられた状況で考慮されます。 あるいは1次以上の要素への再代入において、右辺に構造体や配列の式が与えられた状況で考慮されます。
上記の例では「const varia」という並びですが、他にもconst文にconstが付いて「const const」という並びになったり、 varia文にconstが付いて「varia const」という並びになったりと、様々なパターンが登場します。 これ以降、それらを単に「const varia」、「const const」、「varia const」などと記述するものとします。
上記の例ではconst variaとダブルモディファイアの形をしてはいますが、 第1モディファイアがconstであるため、これは文としてはconst文の範疇とみなされます。 そのため初期化は必須です。 例えば上記のように「const varia a Info[] = v」といった初期化が可能です。
正確に言えばconst variaで宣言した変数の右辺として指定できるのは
const varia、immut varia、tight varia、varia varia
となります。
ここにはまだ紹介していないものも含まれますが、それらについては後述します。
「const varia X」という形式では、X は「再代入禁止」並びに「要素数一定」並びに「集成体不変」で規定されます。 上記の例では a がこの X に該当しますので、 再代入「a = v」や関数呼び出し「Rrk_push_bk( a, c[0] )」、再代入「a[0] = v[0]」は許可されません。
2番目のvariaの指定は、Xが1次以上の要素を持つ場合で意味を持ち、 1次以降のすべての要素を「varia受容性」で規定します。 ただし「const varia X」という形式では、0次要素が(第1モディファイアで)「集成体不変」に規定され、 それにより1次要素への再代入が間接的に禁止されます。 そのため「varia受容性」が実質的に有効になるのは2次以上の要素になります。 上記の例で「a[0].m = 9」では、aは「集成体不変」ですが a[0] は「集成体不変」ではなく、しかも「a[0].m」は「varia受容性」が規定されるため、 許可されます。
尚、初期化子については、その要素についても通常の初期化の規則に当て嵌めて考えて構いません。 上記の例で「const varia d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがvariaであるためvaria構造体とみなされ、許可されます。 一方、上記の例で「const varia e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはvaria構造体とみなされ、許可されません。
別の例も見てみましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
const const a Info[] = v /* OK */
//a[0] = v[0] /* compile error. */
//a[0].m = 9 /* compile error. */
const const d Info[] = [ v[0] ] /* OK */
const const e Info[] = [ c[0] ] /* OK */
今度の例ではconst文にconstモディファイアを指定し、配列を宣言しています。 つまりconst constのパターンです。
const文ですので初期化は必須です。 const constの場合、右辺にいかなるものが来ても初期化可能です。
「const const X」という形式では、X は「再代入禁止」並びに「要素数一定」並びに「集成体不変」で規定されます。 上記の例では a がこの X に該当しますので、 再代入「a = v」や関数呼び出し「Rrk_push_bk( a, c[0] )」、再代入「a[0] = v[0]」は許可されません。
2番目のconstの指定は、Xが1次以上の要素を持つ場合で意味を持ち、 1次以降のすべての要素を「再代入禁止」並びに「要素数一定」並びに「集成体不変」で規定します。 上記の例で「a[0].m = 9」は、a[0].m が「再代入禁止」であるため許可されません。
尚、初期化子については、その要素についても通常の初期化の規則に当て嵌めて考えて構いません。 上記の例で「const const d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがconstであるためconst構造体とみなされ、許可されます。 また、上記の例で「const const e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはconst構造体とみなされ、許可されます。
実は上記は、単にモディファイアなしでconst文を宣言したのと全く同じ制限です。
const文において「const」モディファイアを指定する場合、これを省略できます。
換言すればconst文でモディファイアを省略した場合、デフォルトでは「const」モディファイアが指定されたものとみなされます。
今まで登場したモディファイアのないconst文は、厳密には「const const」が省略されて「const」となったものであったわけです。
const文にはtightモディファイアを指定することもできます。 例えば以下の通りです。
array IAry int[]
varia f IAry[] = [ [1], [2], ]
const tight a IAry[] = f
//a = f /* compile error */
//a[0] = f[0] /* compile error */
//Rrk_push_bk( a[0], 9 ) /* compile error. a[0] is tight array */
const tight d IAry[] = [ f[0] ] /* OK */
const c IAry = [ 0 ]
//const tight e IAry[] = [ c ] /* compile error. */
今度の例ではconst文にtightモディファイアを指定し、配列を宣言しています。 つまりconst tightのパターンです。
const文ですので初期化は必須です。
「const tight X」という形式では、X は「再代入禁止」並びに「要素数一定」並びに「集成体不変」で規定されます。 上記の例では a がこの X に該当しますので、 再代入「a = f」や関数呼び出し「Rrk_push_bk( a, f[0] )」、再代入「a[0] = f[0]」は許可されません。
2番目のtightの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「tight受容性」並びに「要素数一定」で規定します。 ただし「const tight X」という形式では、0次要素が(第1モディファイアで)「集成体不変」に規定され、 それにより1次要素の再代入が間接的に禁止されます。 そのため「tight受容性」が実質的に有効になるのは2次以上の要素になります。 上記の例で関数呼び出し「Rrk_push_bk( a[0], 9 )」は a[0] が「要素数一定」とみなされるため、許可されません。
尚、初期化子については、その要素についても通常の初期化の規則に当て嵌めて考えて構いません。 上記の例で「const tight d IAry[] = [ f[0] ] 」といった初期化は f[0]がvariaであり、d[0]がtightですので、許可されます。 また、上記の例で「const tight e IAry[] = [ c ]」といった初期化は cがconstであり、e[0]がtightですので、許可されません。
第2モディファイアは単なる int型、real型、bool型、conststr型、string型(非コンテナ型)では基本的に意味を持ちません。
例えば、以下のようにint型に対してダブルモディファイアを指定しても、第2モディファイアは単に無視されるということです。
const const ival int = 0 /* 2番目のconstはこの場合意味を持たない */
const文のモディファイア(演習問題)
const文の説明の仕上げとして多次元配列や何層にもネストされた構造体を用いた場合を考えましょう。 例えば以下をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
const const p InfoAry[] = [ [ {1} ] ]
const info Info = {}
//p[0] = [ {} ] /* compile error. */
//p[0][0] = info /* compile error. */
//p[0][0].m = 9 /* compile error. */
上記では構造体の二次元配列pをconst constにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素であるp[0]、p[0][0]、p[0][0].mは(第2モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p[0] = [ {} ]」は(p[0]が「再代入禁止」ですので)許可されません。 また、上記で再代入「p[0][0] = info」は(p[0][0]が「再代入禁止」ですので)許可されません。 また、上記で再代入「p[0][0].m = 9」は(p[0][0].mが「再代入禁止」ですので)許可されません。
多次元配列ではなく何層にもネストされた構造体のメンバアクセスについても同様です。 例えば以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
const varia p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
varia subb SubB = {}
//p = info /* compile error. */
//p.a = suba /* compile error. */
p.a.b = subb /* OK */
p.a.b.m = 9 /* OK */
上記ではネストされた構造体pをconst variaにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「varia受容性」が規定されます。
従って、上記で再代入「p = info」は(pが「再代入禁止」ですので)許可されません。 また、再代入「p.a = suba」は(pが「集成体不変」ですので「p.a=X」という形式に当てはまり)許可されません。 また、再代入「p.a.b = subb」は、p.aは「集成体不変」ではありませんので「p.a.b=X」という形式の代入が一旦許可され、 さらにp.a.bに「varia受容性」があり、右辺のsubbはvaria構造体であるため、許可されます。 最後に再代入「p.a.b.m = 9」は(p.a.bは「集成体不変」ではなく、「varia受容性」の規定からも問題ありませんので)許可されます。
const tightでの例も見ておきましょう。 例えば以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
const tight p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
varia subb SubB = {}
//p = info /* compile error. */
//p.a = suba /* compile error. */
p.a.b = subb /* OK */
p.a.b.m = 9 /* OK */
上記ではネストされた構造体pをconst tightにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「tight受容性」並びに「要素数一定」が規定されます。
従って、上記で再代入「p = info」は(pが「再代入禁止」ですので)許可されません。 また、再代入「p.a = suba」は(pが「集成体不変」ですので「p.a=X」という形式に当てはまり)許可されません。 また、再代入「p.a.b = subb」は、p.aが「集成体不変」ではありませんので「p.a.b=X」という形式が一旦許可され、 さらに左辺のp.a.bが「tight受容性」であり、右辺のsubbがvaria構造体ですので最終的に許可されます。 最後に再代入「p.a.b.m = 9」は(p.a.bが「集成体不変」ではなく、「tight受容性」の規定からも問題ありませんので)許可されます。
immut文のモディファイア
immut文もconst文と同様にモディファイアを指定することができます。 これは「immut モディファイア 識別子 型名」といった書式になります(ただし初期化は必須です)。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
immut varia a Info[] = v
a = v /* OK */
//a[0] = v[0] /* compile error */
//a[0] = c[0] /* compile error */
//Rrk_push_bk( a, v[0] ) /* compile error. a is immut array */
a[0].m = 9 /* OK */
immut varia d Info[] = [ v[0] ] /* OK */
//immut varia e Info[] = [ c[0] ] /* compile error */
上記の例ではimmut文にvariaモディファイアを指定し、配列を宣言しています。 つまりimmut variaのパターンです。
「immut varia X」という形式では、X は「immut受容性」並びに「要素数一定」並びに「集成体不変」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a[0] = v[0]」や「a[0] = c[0]」は「集成体不変」に違反し、関数呼び出し「Rrk_push_bk( a, v[0] )」は「要素数一定」に違反するため許可されません。 一方、再代入「a = v」については、a は「再代入禁止」ではないため許可されます(ここはconst文との大きな違いです)。
2番目のvariaの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「varia受容性」で規定します。 ただしimmut文では、0次要素が(第1モディファイアで)「集成体不変」に規定され、 それにより1次要素の再代入が間接的に禁止されます。 そのため「varia受容性」が実質的に有効になるのは2次以上の要素になります。
また、上記の例で「a[0].m = 9」は、a[0] が「集成体不変」ではなく、a[0].m が「varia受容性」の規定からも左辺として問題ないため許可されます。
「varia受容性」によれば、const intからvaria intへの再代入は可能ですので、
「a[0].m = 9」といったint型の再代入は許可されることになります。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「immut varia d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがvariaであるためvaria構造体とみなされ、許可されます。 一方、上記の例で「immut varia e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはvaria構造体とみなされ、許可されません。
別の例も見てみましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
immut const a Info[] = v
a = v /* OK */
a = c /* OK */
//a[0] = v[0] /* compile error */
//a[0] = c[0] /* compile error */
//Rrk_push_bk( a, v[0] ) /* compile error */
//a[0].m = 9 /* compile error */
immut const d Info[] = [ v[0] ] /* OK */
immut const e Info[] = [ c[0] ] /* OK */
今度の例ではimmut文にconstモディファイアを指定し、配列を宣言しています。 つまりimmut constのパターンです。
immut constで宣言した変数の右辺には任意のものが指定できます。
「immut const X」という形式では、X は「immut受容性」並びに「要素数一定」並びに「集成体不変」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a[0] = v[0]」や「a[0] = c[0]」は「集成体不変」に違反し、関数呼び出し「Rrk_push_bk( a, v[0] )」は「要素数一定」に違反するため許可されません。 一方、再代入「a = v」や「a = c」については、a は「再代入禁止」ではないため許可されます。
2番目のconstの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「再代入禁止」並びに「要素数一定」並びに「集成体不変」で規定します。 従って、上記の例で再代入「a[0].m = 9」は、2次要素a[0].m について「再代入禁止」ですから、許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「immut const d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については(第2モディファイアがconstであるため)const構造体とみなされ、許可されます。 一方、上記の例で「immut const e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはconst構造体とみなされ、許可されます。
別の例も見てみましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
immut immut a Info[] = v
a = v /* OK */
a = c /* OK */
//a[0] = v[0] /* compile error */
//a[0] = c[0] /* compile error */
//a[0].m = 9 /* compile error */
immut immut d Info[] = [ v[0] ] /* OK */
immut immut e Info[] = [ c[0] ] /* OK */
今度の例ではimmut文にimmutモディファイアを指定し、配列を宣言しています。 つまりimmut immutのパターンです。
「immut immut X」という形式では、X は「immut受容性」並びに「要素数一定」並びに「集成体不変」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a[0] = v[0]」や「a[0] = c[0]」は「集成体不変」に違反し、関数呼び出し「Rrk_push_bk( a, v[0] )」は「要素数一定」に違反するため許可されません。 一方、再代入「a = v」や「a = c」については、a は「再代入禁止」ではないため許可されます。
2番目のimmutの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「immut受容性」並びに「要素数一定」並びに「集成体不変」で規定します。 従って、上記の例で再代入「a[0].m = 9」は、1次要素 a[0] については「集成体不変」が適用されますから、 許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「immut immut d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については(第2モディファイアがimmutであるため)immut構造体とみなされ、許可されます。 一方、上記の例で「immut immut e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはimmut構造体とみなされ、許可されます。
実は上記は、単にモディファイアなしでimmut文を宣言したのと全く同じ制限です。
immut文において「immut」モディファイアを指定する場合、これを省略できます。
換言すればimmut文でモディファイアを省略した場合、デフォルトでは「immut」モディファイアが指定されたものとみなされます。
今まで登場したモディファイアのないimmut文は、厳密には「immut immut」が省略されて「immut」となったものであったわけです。
immut文にはtightモディファイアを指定することもできます。 例えば以下の通りです。
array IAry int[]
varia f IAry[] = [ [1], [2], ]
const c IAry[] = [ [1], [2], ]
immut tight a IAry[] = f
//a[0] = f[0] /* compile error */
//a[0] = c[0] /* compile error */
//Rrk_push_bk( a, f[0] ) /* compile error */
a = f /* OK */
//a = c /* compile error */
//Rrk_push_bk( a[0], 9 ) /* compile error */
immut tight d IAry[] = [ f[0] ] /* OK */
//immut tight e IAry[] = [ c[0] ] /* compile error */
今度の例ではimmut文にtightモディファイアを指定し、配列を宣言しています。 つまりimmut tightのパターンです。
「immut tight X」という形式では、X は「immut受容性」並びに「要素数一定」並びに「集成体不変」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a[0] = f[0]」や「a[0] = c[0]」は「集成体不変」に違反し、関数呼び出し「Rrk_push_bk( a, f[0] )」は「要素数一定」に違反するため許可されません。 一方、再代入「a = f」については、a は「再代入禁止」ではないため許可されます。 再代入「a = c」については、a は「再代入禁止」ではありませんが、 ダブルモディファイアのアサイナビリティの規則に違反し許可されません。
詳しくは後の項で述べますが、これはaの第2モディファイアがtight、
cの第2モディファイアがconstであり、tight受容性に反するためです。
2番目のtightの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「tight受容性」並びに「要素数一定」で規定します。 従って、上記の例で関数呼び出し「Rrk_push_bk( a[0], 9 )」は、a[0]が「要素数一定」であるため許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「immut tight d IAry[] = [ f[0] ] 」といった初期化は f[0]がvaria配列、d[0]については(第2モディファイアがtightであるため)tight配列とみなされ、許可されます。 一方、上記の例で「immut tight e IAry[] = [ c[0] ]」といった初期化は c[0]がconst配列、e[0]についてはtight配列とみなされ、許可されません。
immut文のモディファイア(演習問題)
immut文の説明の仕上げとして多次元配列や何層にもネストされた構造体を用いた場合を考えましょう。 例えば以下をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
immut immut p InfoAry[] = [ [ {1} ] ]
const info Info = {}
p = [ [ {2} ] ]
//p[0] = [ {} ] /* compile error */
//p[0][0] = info /* compile error */
//p[0][0].m = 9 /* compile error */
上記では構造体の二次元配列pをimmut immutにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p[0]、p[0][0]、p[0][0].m は(第2モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p = [ [ {2} ] ]」は、(pが「immut受容性」で規定されていますので)許可されます。 また、上記で再代入「p[0] = [ {} ]」は(pが「集成体不変」より)許可されません。 また、上記で再代入「p[0][0] = info」は(p[0]が「集成体不変」より)許可されません。 また、上記で再代入「p[0][0].m = 9」は(p[0][0]が「集成体不変」より)許可されません。
次に何層にもネストされた構造体のメンバアクセスについて見ます。 まずはimmut immutの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
immut immut p Info = { { { 1 } } }
const info Info = {}
const suba SubA = {}
const subb SubB = {}
p = info /* OK */
//p.a = suba /* compile error */
//p.a.b = subb /* compile error */
//p.a.b.m = 9 /* compile error */
上記ではネストされた構造体pをimmut immutにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p.a、p.a.b は(第2モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p = info」は(pが「immut受容性」、infoがconstですので)許可されます。 また、上記で再代入「p.a = suba」は(pが「集成体不変」より)許可されません。 また、上記で再代入「p.a.b = subb」は(p.aが「集成体不変」より)許可されません。 また、上記で再代入「p.a.b.m = 9」は(p.a.bが「集成体不変」より)許可されません。
次にimmut tightの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
immut tight p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
const subb SubB = {}
p = info /* OK */
//p.a = suba /* compile error */
//p.a.b = subb /* compile error */
p.a.b.m = 9 /* OK */
上記ではネストされた構造体pをimmut tightにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「tight受容性」並びに「要素数一定」が規定されます。
従って、上記で再代入「p = info」は(pが「immut受容性」、infoがconstですので)許可されます。 また、上記で再代入「p.a = suba」は(pが「集成体不変」より)許可されません。 また、上記で再代入「p.a.b = subb」は(p.a.bが「tight受容性」、subbがconstですので)許可されません。 また、上記で再代入「p.a.b.m = 9」は(p.a.b.mが「tight受容性」、型がintですので)許可されます。
次にimmut variaの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
immut varia p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
varia subb SubB = {}
p = info /* OK */
//p.a = suba /* compile error */
p.a.b = subb /* OK */
//p.a.b.m = 9 /* compile error */
上記ではネストされた構造体pをimmut variaにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。 また1次以降の要素である p.a、p.a.b は(第2モディファイアにより) 「varia受容性」が規定されます。
従って、上記で再代入「p = info」は(pが「immut受容性」、infoがconstですので)許可されます。 また、上記で再代入「p.a = suba」は(pが「集成体不変」より)許可されません。 また、上記で再代入「p.a.b = subb」は(p.a.bが「varia受容性」であり、subbはvaria構造体ですので)許可されます。 また、上記で再代入「p.a.b.m = 9」は(p.a.b.mが「varia受容性」であり、型がintですので)許可されます。
上記では、
varia構造体subbを、「varia受容性」で規定された要素p.a.bに再代入することが可能でしたが、
もし仮にsubbがconst構造体であった場合、(p.a.bは「varia受容性」で規定されているため)subbをp.a.bに再代入することはできません。
仮にこれ再代入する意図で、immut variaの替わりにimmut constやimmut immutと書いたとしても、
その意図通りには機能しません。
n次要素にconstやimmutを指定した時点で「集成体不変」が規定され、 n+1次要素への再代入が禁止されることに注意してください。 そのため、const文やimmut文の時点で1次要素への再代入は不可となります。 また第2モディファイアにconstを指定した場合は1次以上の全ての要素への代入は不可となり、 さらに第2モディファイアにimmutを指定した場合は2次以上の全ての要素への代入は不可となります。
一方、後に述べるtight文やvaria文において第2モディファイアにimmutを指定した場合 (つまりtight immut、varia immutの場合)、 1次要素にimmut受容性が規定されるため、const構造体を1次要素に再代入することが可能です。 ただし、第2モディファイアのimmutは1次以降の要素を「集成体不変」にも規定するため、2次以降の要素への代入は禁止されます。
n次要素にconstやimmutを指定した時点で「集成体不変」が規定され、 n+1次要素への再代入が禁止されることに注意してください。 そのため、const文やimmut文の時点で1次要素への再代入は不可となります。 また第2モディファイアにconstを指定した場合は1次以上の全ての要素への代入は不可となり、 さらに第2モディファイアにimmutを指定した場合は2次以上の全ての要素への代入は不可となります。
そのため const constとconst immut のいずれもすべての次数の要素への再代入が禁止され、
この二つは実質同じ制限を与えるものになります。
また immut constとimmut immut のいずれも0次以外のすべての要素への再代入が禁止され、
この二つは実質同じ制限を与えるものになります。
一方、後に述べるtight文やvaria文において第2モディファイアにimmutを指定した場合 (つまりtight immut、varia immutの場合)、 1次要素にimmut受容性が規定されるため、const構造体を1次要素に再代入することが可能です。 ただし、第2モディファイアのimmutは1次以降の要素を「集成体不変」にも規定するため、2次以降の要素への代入は禁止されます。
tight文のモディファイア
tight文もconst文と同様にモディファイアを指定することができます。 これは「tight モディファイア 識別子 型名」といった書式になります(ただし初期化は必須です)。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
tight varia a Info[] = v
a = v /* OK */
a[0] = v[0] /* OK */
//a[0] = c[0] /* compile error. */
//Rrk_push_bk( a, v[0] ) /* compile error. a is tight array */
a[0].m = 9 /* OK */
tight varia d Info[] = [ v[0] ] /* OK */
//tight varia e Info[] = [ c[0] ] /* compile error */
上記の例ではtight文にvariaモディファイアを指定し、配列を宣言しています。 つまりtight variaのパターンです。
「tight varia X」という形式では、X は「要素数一定」並びに「tight受容性」で規定されます。 従って上記の例では a が X に該当しますので、 「Rrk_push_bk( a, c[0] )」といった関数呼び出しはこの規定に違反し許可されません。 また上記の例では a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のvariaの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「varia受容性」で規定します。 今「a[0]」はvaria構造体、v[0]はvaria構造体、c[0]はconst構造体です。 「varia受容性」に準じて考えますと、結果的に「a[0] = v[0]」といった再代入は許可されますが、 「a[0] = c[0]」といった再代入は許可されないことになります。
また、上記の例で「a[0].m = 9」は、a[0] が「集成体不変」ではなく、a[0].m が「varia受容性」の規定からも左辺として問題ないため許可されます。
「varia受容性」によれば、const intからvaria intへの再代入は可能ですので、
「a[0].m = 9」といったint型の再代入は許可されることになります。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「tight varia d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがvariaであるためvaria構造体とみなされ、許可されます。 一方、上記の例で「tight varia e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはvaria構造体とみなされ、許可されません。
別の例も見てみましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
tight const a Info[] = v
//a[0] = v[0] /* compile error */
//a[0] = c[0] /* compile error */
//a[0].m = 9 /* compile error */
tight const d Info[] = [ v[0] ] /* OK */
tight const e Info[] = [ c[0] ] /* OK */
今度の例ではtight文にconstモディファイアを指定し、配列を宣言しています。 つまりtight constのパターンです。
「tight const X」という形式では、X は「要素数一定」並びに「tight受容性」で規定されます。 従って上記の例では a が X に該当しますので、 「Rrk_push_bk( a, c[0] )」といった関数呼び出しはこの規定に違反し許可されません。 また上記の例では a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のconstの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「再代入禁止」と「要素数一定」と「集成体不変」で規定します。 そのため結果的には「a[0] = v[0]」、「a[0] = c[0]」といった再代入はすべて禁止されることになります。
また、上記の例で「a[0].m = 9」は、1次要素 a[0] については「集成体不変」が適用されますし、 2次要素 a[0].m については「再代入禁止」が適用されますから、いずれにしてもこの再代入は許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「tight const d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがconstであるためconst構造体とみなされ、許可されます。 一方、上記の例で「tight const e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはconst構造体とみなされ、許可されます。
別の例も見てみましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
tight immut a Info[] = v
a[0] = v[0] /* OK */
a[0] = c[0] /* OK */
//a[0].m = 9 /* compile error */
tight immut d Info[] = [ v[0] ] /* OK */
tight immut e Info[] = [ c[0] ] /* OK */
今度の例ではtight文にimmutモディファイアを指定し、配列を宣言しています。 つまりtight immutのパターンです。
「tight immut X」という形式では、X は「要素数一定」並びに「tight受容性」で規定されます。 従って上記の例では a が X に該当しますので、 「Rrk_push_bk( a, c[0] )」といった関数呼び出しはこの規定に違反し許可されません。 また上記の例では a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のimmutの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「immut受容性」と「要素数一定」と「集成体不変」で規定します。 そのため「immut受容性」により「a[0] = v[0]」、「a[0] = c[0]」といった再代入は許可されます。
また、上記の例で「a[0].m = 9」は、1次要素 a[0] については「集成体不変」が適用されますから、 この再代入は許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「tight immut d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については第2モディファイアがimmutであるためimmut構造体とみなされ、許可されます。 一方、上記の例で「tight immut e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはimmut構造体とみなされ、許可されます。
tight文にはtightモディファイアを指定することもできます。 例えば以下の通りです。
array IAry int[]
varia f IAry[] = [ [1], [2], ]
const c IAry[] = [ [1], [2], ]
tight tight a IAry[] = f
a = f /* OK */
//Rrk_push_bk( a, f[0] ) /* compile error */
a[0] = f[0] /* OK */
//a[0] = c[0] /* compile error */
//Rrk_push_bk( a[0], 9 ) /* compile error */
tight tight d IAry[] = [ f[0] ] /* OK */
//tight tight e IAry[] = [ c[0] ] /* compile error */
今度の例ではtight文にtightモディファイアを指定し、配列を宣言しています。 つまりtight tightのパターンです。
「tight tight X」という形式では、X は「要素数一定」並びに「tight受容性」で規定されます。 従って上記の例では a が X に該当しますので、 「Rrk_push_bk( a, f[0] )」といった関数呼び出しはこの規定に違反し許可されません。 また上記の例では a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のtightの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「tight受容性」並びに「要素数一定」で規定します。 今「a[0]」はtight、f[0]はvaria、c[0]はconstです。 「tight受容性」に準じて考えますと、結果的に「a[0] = f[0]」といった再代入は許可され、 「a[0] = c[0]」といった再代入は許可されないことになります。
また、上記の例で「Rrk_push_bk( a[0], 9 )」といった関数呼び出しは、a[0]が「要素数一定」であるため許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「tight tight d IAry[] = [ f[0] ] 」といった初期化は f[0]がvaria配列、d[0]については(第2モディファイアがtightであるため)tight配列とみなされ、許可されます。 一方、上記の例で「tight tight e IAry[] = [ c[0] ]」といった初期化は c[0]がconst配列、e[0]についてはtight配列とみなされ、許可されません。
実は上記は、単にモディファイアなしでtight文を宣言したのと全く同じ制限です。
tight文において「tight」モディファイアを指定する場合、これを省略できます。
換言すればtight文でモディファイアを省略した場合、デフォルトでは「tight」モディファイアが指定されたものとみなされます。
今まで登場したモディファイアのないtight文は、厳密には「tight tight」が省略されて「tight」となったものであったわけです。
tight文のモディファイア(演習問題)
tight文の説明の仕上げとして多次元配列や何層にもネストされた構造体を用いた場合を考えましょう。 例えば以下をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
tight immut p InfoAry[] = [ [ {1} ] ]
const info Info = {}
//p[0][0] = info /* compile error */
p[0] = [ {} ] /* OK */
//p[0][0].m = 9 /* compile error */
上記では構造体の二次元配列pをtight immutにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「tight受容性」並びに「要素数一定」となります。 また1次以降の要素であるp[0]、p[0][0]、p[0][0].m は(第2モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で「p[0][0] = info」といった代入は(p[0]が「集成体不変」により)許可されません。 また、上記で「p[0] = [ {} ]」といった代入は(p[0]が「immut受容性」で規定されていますので)許可されます。 また、上記で「p[0][0].m = 9」といった代入は(p[0][0]が「集成体不変」により)許可されません。
次に何層にもネストされた構造体のメンバアクセスについて見ます。 まずはtight immutの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
tight immut p Info = { { { 1 } } }
const info Info = {}
const suba SubA = {}
const subb SubB = {}
//p = info /* compile error */
p.a = suba /* OK */
//p.a.b = subb /* compile error */
//p.a.b.m = 9 /* compile error */
上記ではネストされた構造体pをtight immutにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「tight受容性」並びに「要素数一定」となります。 また1次以降の要素である p.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」が規定されます。
従って、上記で再代入「p = info」は(pが「tight受容性」、infoがconstですので)許可されません。 また、再代入「p.a = suba」は(p.aが「immut受容性」、subaがconstですので)許可されます。 また、再代入「p.a.b = subb」は(p.aが「集成体不変」ですので)許可されません。 最後に再代入「p.a.b.m = 9」は(p.a.bが「集成体不変」ですので)許可されません。
const/immut文ではconst構造体を要素として再代入する手段は提供されませんでしたが、
tight文ではこのようにconst構造体を要素として再代入する手段が提供されます。
次にtight tightの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
tight tight p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
const subb SubB = {}
//p = info /* compile error. */
p.a = suba /* OK */
//p.a.b = subb /* compile error */
p.a.b.m = 9 /* OK */
上記ではネストされた構造体pをtight tightにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「tight受容性」並びに「要素数一定」となります。 また1次以降の要素である p.a、p.a.b は(第2モディファイアにより) 「tight受容性」並びに「要素数一定」が規定されます。
従って、上記で再代入「p = info」は(pが「tight受容性」、infoがconstですので)許可されません。 また、再代入「p.a = suba」は(p.aが「tight受容性」、subaがvariaですので)許可されます。 また、再代入「p.a.b = subb」は(p.a.bが「tight受容性」、subbがconstですので)許可されません。 最後に再代入「p.a.b.m = 9」は(p.a.b.mが「tight受容性」ですので)許可されます。
次にtight variaの場合を考えます。 以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
tight varia p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
const subb SubB = {}
//p = info /* compile error */
p.a = suba /* OK */
//p.a.b = subb /* compile error */
p.a.b.m = 9 /* OK */
上記ではネストされた構造体pをtight variaにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「tight受容性」並びに「要素数一定」となります。 また1次以降の要素である p.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「varia受容性」が規定されます。
従って、上記で再代入「p = info」は(pが「tight受容性」、infoがconstですので)許可されません。 また、再代入「p.a = suba」は(p.aが「varia受容性」、subaがvariaですので)許可されます。 また、再代入「p.a.b = subb」は(p.a.bが「varia受容性」、subbがconstですので)許可されません。 最後に再代入「p.a.b.m = 9」は(p.a.b.mが「varia受容性」で規定されていますので)許可されます。
varia文のモディファイア
varia文についてもダブルモディファイアを指定することができます。 これは「varia モディファイア 識別子 型名」といった書式になります。
まずvaria constのパターンをみましょう。
struct Info {
m int
}
varia const a Info[]
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
a = v /* OK */
Rrk_push_bk( a, c[0] ) /* OK */
//a[0] = v[0] /* compile error */
//a[0] = c[0] /* compile error */
//a[0].m = 9 /* compile error */
varia const d Info[] = [ v[0] ] /* OK */
varia const e Info[] = [ c[0] ] /* OK */
「varia const X」という形式では、X は「varia受容性」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a = v」や関数呼び出し「Rrk_push_bk( a, c[0] )」は許可されます。 また上記の a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のconstの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「再代入禁止」と「要素数一定」と「集成体不変」で規定します。 そのため結果的には「a[0] = v[0]」、「a[0] = c[0]」といった再代入はすべて禁止されることになります。
また、上記の再代入「a[0].m = 9」は、2次要素 a[0].m についても「再代入禁止」となりますので許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「varia const d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については(第2モディファイアがconstであるため)const構造体とみなされ、許可されます。 一方、上記の例で「varia const e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはconst構造体とみなされ、許可されます。
次にvaria immutのパターンをみましょう。
struct Info {
m int
}
varia immut a Info[]
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
a = v /* OK */
Rrk_push_bk( a, c[0] ) /* OK */
a[0] = v[0] /* OK */
a[0] = c[0] /* OK */
//a[0].m = 9 /* compile error */
varia immut d Info[] = [ v[0] ] /* OK */
varia immut e Info[] = [ c[0] ] /* OK */
「varia immut X」という形式では、X は「varia受容性」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a = v」や関数呼び出し「Rrk_push_bk( a, c[0] )」は許可されます。 また上記の a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のimmutの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「immut受容性」と「要素数一定」と「集成体不変」で規定します。 つまりa[0]はimmut受容性となるため「a[0] = v[0]」、「a[0] = c[0]」といった再代入は許可されます。
また、上記の再代入「a[0].m = 9」は、a[0]が「集成体不変」となりますので許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「varia immut d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については(第2モディファイアがimmutであるため)immut構造体とみなされ、許可されます。 一方、上記の例で「varia immut e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはimmut構造体とみなされ、許可されます。
次にvaria variaのパターンをみましょう。
struct Info {
m int
}
varia varia a Info[]
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
a = v /* OK */
Rrk_push_bk( a, v[0] ) /* OK */
a[0] = v[0] /* OK */
//a[0] = c[0] /* compile error. */
a[0].m = 9 /* OK */
varia varia d Info[] = [ v[0] ] /* OK */
//varia varia e Info[] = [ c[0] ] /* compile error. */
「varia const X」という形式では、X は「varia受容性」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a = v」や関数呼び出し「Rrk_push_bk( a, v[0] )」は許可されます。 また上記の a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のvariaの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「varia受容性」で規定します。 この「varia受容性」に準じて考えますと、結果的に再代入「a[0] = v[0]」は許可され、再代入「a[0] = c[0]」は許可されないことになります。
また、上記の例で「a[0].m = 9」は、2次要素 a[0].m が「varia受容性」ですので許可されます。
「varia受容性」によれば、const intからvaria intへの再代入は可能ですので、
「a[0].m = 9」といったint型の再代入は許可されることになります。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「varia varia d Info[] = [ v[0] ] 」といった初期化は v[0]がvaria構造体、d[0]については(第2モディファイアがvariaであるため)varia構造体とみなされ、許可されます。 一方、上記の例で「varia varia e Info[] = [ c[0] ]」といった初期化は c[0]がconst構造体、e[0]についてはvaria構造体とみなされ、許可されません。
実は上記は、単にモディファイアなしでvaria文を宣言したのと全く同じ制限です。
varia文において「varia」モディファイアを指定する場合、これを省略できます。
換言すればvaria文でモディファイアを省略した場合、デフォルトでは「varia」モディファイアが指定されたものとみなされます。
今まで登場したモディファイアのないvaria文は、厳密には「varia varia」が省略されて「varia」となったものであったわけです。
ここまで読み進められた方であれば、もうかなり慣れてきたかもしれませんが、さらに補足しましょう。
「varia immut a Info[]」と「varia varia a Info[]」では、どちらがa[0]への再代入がしにくいでしょうか?
前者では a[0]は「immut受容性」、後者では「varia受容性」で規定されます。 そして、「immut受容性」の場合はどんな集成体(構造体や配列など)でも再代入できますが、 「varia受容性」の場合はvaria型の集成体でのみ再代入が可能です。 つまり、集成体の場合は、「varia varia a Info[]」の方がa[0]への再代入がしにくいことになります。
次に、「varia immut a int[]」と「varia varia a int[]」では、どちらがa[0]への再代入がしにくいでしょうか? これに関しても同様に、前者では「immut受容性」、後者では「varia受容性」で規定されます。 そして、非集成体(int型など)の場合、 「immut受容性」と「varia受容性」のいずれであっても再代入が可能であり、両者の差はありません。
「varia immut a Info[]」と「varia varia a Info[]」では、どちらがa[0]への再代入がしにくいでしょうか?
前者では a[0]は「immut受容性」、後者では「varia受容性」で規定されます。 そして、「immut受容性」の場合はどんな集成体(構造体や配列など)でも再代入できますが、 「varia受容性」の場合はvaria型の集成体でのみ再代入が可能です。 つまり、集成体の場合は、「varia varia a Info[]」の方がa[0]への再代入がしにくいことになります。
次に、「varia immut a int[]」と「varia varia a int[]」では、どちらがa[0]への再代入がしにくいでしょうか? これに関しても同様に、前者では「immut受容性」、後者では「varia受容性」で規定されます。 そして、非集成体(int型など)の場合、 「immut受容性」と「varia受容性」のいずれであっても再代入が可能であり、両者の差はありません。
次はvaria tightのパターンをみましょう。
array IAry int[]
varia tight a IAry[] = [[]]
varia f IAry[] = [ [1], [2], ]
const c IAry[] = [ [1], [2], ]
a = f /* OK */
a[0] = f[0] /* OK */
//a[0] = c[0] /* compile error */
Rrk_push_bk( a, f[0] ) /* OK */
//Rrk_push_bk( a[0], 9 ) /* compile error. a[0] is tight */
varia tight d IAry[] = [ f[0] ]
//varia tight e IAry[] = [ c[0] ]
「varia tight X」という形式では、X は「varia受容性」で規定されます。 従って上記の例では a が X に該当しますので、 再代入「a = f」や関数呼び出し「Rrk_push_bk( a, f[0] )」は許可されます。 また上記の a は「集成体不変」ではありませんので「a[0] = v[0]」と「a[0] = c[0]」は形式上は一旦許可されます。 これが最終的に「再代入可能であると確定する」には次の第2モディファイアを考慮しなければなりません。
2番目のtightの指定は、Xが1次以上の要素を持つ場合で意味を持ち、1次以降のすべての要素を 「tight受容性」並びに「要素数一定」で規定します。 「tight受容性」に準じて考えますと、結果的に再代入「a[0] = f[0]」は許可され、 再代入「a[0] = c[0]」は許可されないことになります。
また、上記の例で関数呼び出し「Rrk_push_bk( a[0], 9 )」は、a[0]が「要素数不変」ですので許可されません。
尚、初期化子については、対応する要素について通常の初期化の規則に当て嵌めて考えます。 上記の例で「varia tight d IAry[] = [ f[0] ] 」といった初期化は f[0]がvaria配列、d[0]については(第2モディファイアがtightであるため)tight配列とみなされ、許可されます。 一方、上記の例で「varia tight e IAry[] = [ c[0] ]」といった初期化は c[0]がconst配列、e[0]についてはtight配列とみなされ、許可されません。
varia文のモディファイア(演習問題)
varia文の説明の仕上げとして多次元配列や何層にもネストされた構造体を用いた場合を考えましょう。 例えば以下をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
varia const p InfoAry[] = [ [ {1} ] ]
const info Info = {}
p = [ [ {2} ] ] /* OK */
//p[0] = [ {} ] /* compile error */
//p[0][0] = info /* compile error */
//p[0][0].m = 9 /* compile error */
上記では構造体の二次元配列pをvaria constにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「varia受容性」となります。 また1次以降の要素であるp[0]、p[0][0]、p[0][0].m は(第2モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p = [ [ {2} ] ]」は(pが「varia受容性」より)許可されます。 また、上記で再代入「p[0] = [ {} ]」は(p[0]が「再代入禁止」より)許可されません。 また、上記で再代入「p[0][0] = info」は(p[0][0]が「再代入禁止」より)許可されません。 また、上記で再代入「p[0][0].m = 9」は(p[0][0].mが「再代入禁止」より)許可されません。
多次元配列ではなく何層にもネストされた構造体のメンバアクセスについても同様です。
まずvaria constの場合を考えます。 例えば以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
varia const p Info = { { { 1 } } }
const info Info = {}
varia suba SubA = {}
const subb SubB = {}
//p = info /* compile error */
//p.a = suba /* compile error */
//p.a.b = subb /* compile error */
//p.a.b.m = 9 /* compile error */
上記ではネストされた構造体pをvaria constにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「varia受容性」となります。 また1次以降の要素であるp.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「再代入禁止」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p = info」は(pが「varia受容性」で、infoがconstですので)許可されません。 また、上記で再代入「p.a = suba」は(p.aが「再代入禁止」より)許可されません。 また、上記で再代入「p.a.b = subb」は(p.a.bが「再代入禁止」より)許可されません。 また、上記で再代入「p.a.b.m = 9」は(p.a.b.mが「再代入禁止」より)許可されません。
次にvaria immutの場合を考えます。 例えば以下をご覧下さい。
struct SubB {
m int
}
struct SubA {
b SubB
}
struct Info {
a SubA
}
varia immut p Info = { { { 1 } } }
const info Info = {}
const suba SubA = {}
const subb SubB = {}
//p = info /* compile error */
p.a = suba /* OK */
//p.a.b = subb /* compile error */
//p.a.b.m = 9 /* compile error */
上記ではネストされた構造体pをvaria immutにより宣言しています。 この場合、(0次要素である)pは(第1モディファイアにより) 「varia受容性」となります。 また1次以降の要素であるp.a、p.a.b、p.a.b.m は(第2モディファイアにより) 「immut受容性」並びに「要素数一定」並びに「集成体不変」となります。
従って、上記で再代入「p = info」は(pが「varia受容性」で、infoがconstですので)許可されません。 また、上記で再代入「p.a = suba」は(p.aが「immut受容性」より)許可されます。 また、上記で再代入「p.a.b = subb」は(p.aが「集成体不変」より)許可されません。 また、上記で再代入「p.a.b.m = 9」は(p.a.bが「集成体不変」より)許可されません。
const/immut文ではconst構造体を要素として再代入する手段は提供されませんでしたが、
varia文ではこのようにconst構造体を要素として再代入する手段が提供されます。
ここまでのまとめ
これまでのまとめを表にしたものを以下に示します。
- 要素が、配列や構造体のような集成体の場合
- 要素が、int型、real型、bool型、conststr/string型などの非集成体の場合
| 第1モディファイア | 第2モディファイア | 要素を再代入する場合の規則 |
|---|---|---|
| const | const | 再代入は完全に禁止 |
| const | immut | 再代入は完全に禁止 |
| const | tight | 1次要素までの再代入は禁止、2次以上の要素の再代入は「tight受容性」で判定 |
| const | varia | 1次要素までの再代入は禁止、2次以上の要素の再代入は「varia受容性」で判定 |
| immut | const | 0次要素の再代入は「immut受容性」で判定。1次以上の要素の再代入は禁止 |
| immut | immut | 0次要素の再代入は「immut受容性」で判定。1次以上の要素の再代入は禁止 |
| immut | tight | 0次要素の再代入は「immut受容性」で判定。1次要素の再代入は禁止。2次以上の要素の再代入は「tight受容性」で判定 |
| immut | varia | 0次要素の再代入は「immut受容性」で判定。1次要素の再代入は禁止。2次以上の要素の再代入は「varia受容性」で判定 |
| tight | const | 0次要素の再代入は「tight受容性」で判定。1次以上の要素の再代入は禁止 |
| tight | immut | 0次要素の再代入は「tight受容性」で判定。1次要素の再代入は「immut受容性」で判定。2次以上の要素の再代入は禁止 |
| tight | tight | 0次要素の再代入は「tight受容性」で判定。1以上の要素の再代入は「tight受容性」で判定 |
| tight | varia | 0次要素の再代入は「tight受容性」で判定。1以上の要素の再代入は「varia受容性」で判定 |
| varia | const | 0次要素の再代入は「varia受容性」で判定。1次以上の要素の再代入は禁止 |
| varia | immut | 0次要素の再代入は「varia受容性」で判定。1次要素の再代入は「immut受容性」で判定。2次以上の要素の再代入は禁止 |
| varia | tight | 0次要素の再代入は「varia受容性」で判定。1次以上の要素の再代入は「tight受容性」で判定 |
| varia | varia | 0次要素の再代入は「varia受容性」で判定。1次以上の要素の再代入は「varia受容性」で判定 |
| 第1モディファイア | 第2モディファイア | 要素を再代入する場合の規則 |
|---|---|---|
| const | const | 再代入禁止 |
| const | immut | 再代入禁止 |
| const | tight | 再代入禁止 |
| const | varia | 再代入禁止 |
| immut | const | 再代入可能 |
| immut | immut | 再代入可能 |
| immut | tight | 再代入可能 |
| immut | varia | 再代入可能 |
| tight | const | 再代入可能 |
| tight | immut | 再代入可能 |
| tight | tight | 再代入可能 |
| tight | varia | 再代入可能 |
| varia | const | 再代入可能 |
| varia | immut | 再代入可能 |
| varia | tight | 再代入可能 |
| varia | varia | 再代入可能 |
- 単にconstと書いた場合、「const const」と同じ
- 単にtightと書いた場合、「tight tight」と同じ
- 単にvariaと書いた場合、「varia varia」と同じ
- 単にreferと書いた場合、「refer varia」と同じ
- 0次要素については第1モディファイアが規定し、1次以降の要素については第2モディファイアが規定する
- 初期化子の要素については第2モディファイアが規定する
ダブルモディファイアのアサイナビリティ
ダブルモディファイアでは第1モディファイアのアサイナビリティと第2モディファイアのアサイナビリティの両方が問題ない場合のみ、 全体としてのアサイナビリティが許可されます。
例えば「varia immut」から「immut varia」への再代入を考えます。 以下の例をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
varia immut vi InfoAry[] = [ [ { 1 } ] ]
immut varia iv InfoAry[] = [ [ { 2 } ] ]
iv = vi
この場合、第1モディファイアではvariaからimmutですのでimmut受容性より許容されますが、 一方、第2モディファイアではimmutからvariaですのでvaria受容性より許容されません。 第2モディファイアで問題があるため、全体としての再代入も許可されないというわけです。
次に「immut varia」から「varia immut」への再代入を考えます。 以下の例をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
varia immut vi InfoAry[] = [ [ { 1 } ] ]
immut varia iv InfoAry[] = [ [ { 2 } ] ]
vi = iv
この場合、第1モディファイアではimmutからvariaですのでvaria受容性より許容されません。 一方、第2モディファイアではvariaからimmutですのでimmut受容性より許容されます。 第1モディファイアで問題があるため、全体としての再代入も許可されないというわけです。
勿論、第1モディファイアと第2モディファイアの双方で問題がある場合も、
全体としての再代入は許可されません。
この規則に則って考えると、例えばtight tightで宣言した変数の右辺として指定できるのは tight varia、tight tight、varia varia、varia tight となることが導き出せます。 第1モディファイアtightへ再代入可能なのは(tight受容性から)tightとvariaのみであり、 第2モディファイアtightへ再代入可能なのは(tight受容性から)tightとvariaのみですから、 あとはそれらの組み合わせをすべて列挙すればよいことになります。
またこの規則から、immut immutで宣言した変数の右辺には任意のものが指定できることもわかります。 第1モディファイアimmutへ再代入可能なのは(immut受容性から)const/immut/tight/variaすべてであり、 第2モディファイアimmutへ再代入可能なのは(immut受容性から)const/immut/tight/variaすべてですから、 結局すべての組み合わせとなります。
次に右辺が1次以上の要素へのアクセスの式となっている場合の再代入を考えます。 以下の例をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
varia tight vt InfoAry[] = [ [ { 1 } ] ]
immut const ic Info
ic = vt[0][0] /* OK */
immut varia iv Info
//iv = vt[0][0] /* compile error */
上記では、vtはvaria tightで指定されていますから、2次要素であるvt[0][0]はtightで規定されます。 ダブルモディファイアが指定されているのはvtであってvt[0][0]ではないわけですが、 ダブルモディファイアのアサイナビリティを判定する上ではvt[0][0]のダブルモディファイアを規定する必要があります。
この場合vt[0][0]自体のダブルモディファイアはtight tightであると考えます。 つまりvt[0][0]自体に指定されたモディファイアを単純に二つ並べたものがそのダブルモディファイアと考えます。
この規則に則ると、再代入「ic = vt[0][0]」ではtight tightからimmut constへの代入ですから 第1モディファイアと第2モディファイアのどちらも問題なく、全体としての再代入も許可されます。 一方、再代入「iv = vt[0][0]」ではtight tightからimmut variaへの代入ですから 第1モディファイアは問題ありませんが、第2モディファイアではtightからvariaとなり(varia受容性より)問題があり、 全体としての再代入も許可されないことになります。
次に左辺が1次以上の要素へのアクセスの式となっている場合の再代入を考えます。 以下の例をご覧下さい。
struct Info {
m int
}
array InfoAry Info[]
varia tight vt InfoAry[] = [ [ { 1 } ] ]
varia varia vv Info = { 3 }
vt[0][0] = vv /* OK */
varia const vc Info = { 2 }
vt[0][0] = vc /* compile error */
左辺の場合も右辺の場合と同様、vt[0][0]自体のダブルモディファイアはtight tightであると考えます。 つまりvt[0][0]自体に指定されたモディファイアを単純に二つ並べたものがそのダブルモディファイアと考えます。
この規則に則ると、再代入「vt[0][0] = vv」ではvaria variaからtight tightへの代入ですから 第1モディファイアと第2モディファイアのどちらも問題なく、全体としての再代入も許可されます。 一方、再代入「vt[0][0] = vc」ではvaria constからtight tightへの代入ですから 第1モディファイアは問題ありませんが、第2モディファイアではconstからtightとなり(tight受容性より)問題があり、 全体としての再代入も許可されないことになります。
右辺と左辺の両方が1次以上の要素へのアクセスの式となっている場合は、
まず右辺と左辺それぞれのダブルモディファイアを同様の規則で考え、
そのダブルモディファイアのアサイナビリティを考えます。
refer文のモディファイア
refer文もモディファイアを指定することができます。 これは「refer 第1モディファイア 第2モディファイア 識別子 (型名) = 変数」といった書式になります。 これまでとは違い、「refer」キーワードの後にモディファイアを二つ(ダブルモディファイア)を指定することができる点にご注意ください。
モディファイアが三つ並んでトリプルモディファイアとでも呼ぶべき指定のように見えるかもしれませんが、
referはモディファイアとみなさないので、この場合もやはりダブルモディファイアと考えます。
ちなみにRarakuではトリプルモディファイアはありません。
refer文ですので初期化は必須です。 このときrefer文の左辺や右辺の第1モディファイアとしてconstを指定することはできません。 例えば以下はコンパイルエラーとなります。
struct Info {
m int
}
const c Info[] = [ { 2 } ]
refer const varia b = c /* compile error : const varia <= const const */
上記の「refer const varia b = c」は、左辺や右辺の第1モディファイアがconstですので許可されません。
さらに集成体におけるrefer文の初期化では、const/immut/tight/varia文とは異なり、 左辺のダブルモディファイアと右辺のダブルモディファイアは一致していなければなりません。 単にダブルモディファイアのアサイナビリティを満たすだけでは不十分であることにご注意ください。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
tight t Info[] = [ { 3 } ]
refer varia varia a = v /* OK */
//refer tight immut d = t /* compile error */
上記の「refer varia varia a = v」は、左辺がvaria varia、右辺もvaria variaですので、 ダブルモディファイアとして完全に一致し、許可されます。 一方、「refer tight immut d = t」は、左辺がtight immut、右辺がtight tightですから 通常のダブルモディファイアのアサイナビリティとしては問題ないのですが、 ダブルモディファイアが完全には一致してはいないため、集成体のrefer文の初期化としては許可されません。
ただし、immut constからimmut immutへの初期化や immut immutからimmut constへの初期化に限り、(ダブルモディファイアとして完全には一致していませんが)許可されます。 これらは実質的な制限としては同等だからです。 例えば以下の通りです。
struct Info {
m int
}
immut i Info[] = [ { 4 } ]
refer immut const e = i /* OK */
上記の「refer immut const e = i」は、左辺がimmut const、右辺がimmut immutであり、 ダブルモディファイアが完全には一致してはいないですが、この組み合わせは例外的に許可されます。
一方、非集成体におけるrefer文の初期化はconst/immut/tight/varia文と同様に、 通常のダブルモディファイアのアサイナビリティの規則に当て嵌めて考えることができます。 例えば以下の通りです。
varia varia f = 1
refer immut immut g = f /* OK */
上記の「refer immut immut g = f」は、gとfはともにintであり非集成体のrefer文の初期化ですから、 通常のダブルモディファイアのアサイナビリティの規則で考えることができ、この場合許可されます。
また初期化ではなく再代入については、集成体/非集成体のいずれも通常のダブルモディファイアのアサイナビリティ の規則に当て嵌めて考えることができます。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
refer varia varia a = v /* OK */
a = v /* OK */
//a = c /* compile error. */
a[0] = v[0] /* OK */
a[0].m = 9 /* OK */
Rrk_push_bk( a, v[0] ) /* OK */
上記はこれまでと同様に考えることができます。 a は「varia受容性」ですので、再代入「a = v」は許可されます(ただしrefer文ですので代入の意味はvaria文とは異なります)。 また関数呼び出し「Rrk_push_bk(a, v[0])」も許可されます。 一方、「a = c」(const constからvaria varia)といった再代入はダブルモディファイアのアサイナビリティに合致しないため禁止されます。
第2モディファイアはvariaであり、「varia受容性」で規定されますので、 「a[0] = v[0]」は許可され、「a[0] = c[0]」は許可されません。 「a[0].m = 9」は許可されます(「a[0].mは「varia受容性」です)。
referの直後にモディファイアを一つだけ書いた場合、それはそのモディファイアが後ろに二つ指定されたのと同じ意味になります。 例えば「refer varia」とした場合、それは「refer varia varia」と同じ意味です。
一方、「refer」の直後にモディファイアを一つも付けなかった場合、refer文の左辺は右辺のモディファイアと完全に同じになります。 これをモディファイア推論と呼びます。 例えば以下の通りです。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
tight t Info[] = [ { 3 } ]
immut i Info[] = [ { 4 } ]
refer a = v /* OK: a is varia varia */
refer b = c /* OK: b is const const */
refer d = t /* OK: d is tight tight */
refer e = i /* OK: e is immut immut */
上記の「refer a = v」はvがvaria variaですので、aもvaria variaで宣言されます。 また「refer b = c」はcがconst constですので、bもconst constで宣言されます。 また「refer d = t」はtがtight tightですので、dもtight tightで宣言されます。 また「refer e = i」はiがimmut immutですので、eもimmut immutで宣言されます。
参考: refer文の集成体の初期化においてダブルモディファイアを一致させなければならない理由
既に述べた通り、referの左辺(参照変数)のダブルモディファイアと右辺(参照先)の変数のダブルモディファイアは
必ず一致していなければなりません。
このような仕様にしてある理由は、モディファイアのプロテクトの破綻を防ぐためです。
もしも仮にこのような仕様にせず、 集成体においても以下のように通常のダブルモディファイアのアサイナビリティで初期化が可能であったとしましょう。
上記の「refer immut immut a = v」はRarakuでは許可されませんが、仮にこれが許可されたとすると、 すぐ後の再代入「a = c」が問題となります。 a は refer文で宣言していますから、この再代入「a = c」は再代入「v = c」と同じことです。 本来、再代入「v = c」は許可されないはずですが、refer文で宣言した a を介してこのようなことが可能になってしまいます。 これは例えば、次にある「v.m = 9」などで、本来変更されるはずのない c.m が変更されてしまうことになります。
cが「const c = v」などのように擬似定数として宣言された場合であるならばともかく、 上記では「const c = Info[] = [ { 2 } ]」と完全定数として宣言されていますので、 このcの中身が意図せず変更されるような可能性は排除しなければなりません。 そのため、refer文の左辺と右辺のダブルモディファイアを一致させる必要が生じるわけです。
Close
もしも仮にこのような仕様にせず、 集成体においても以下のように通常のダブルモディファイアのアサイナビリティで初期化が可能であったとしましょう。
struct Info {
m int
}
varia v Info[] = [ { 1 } ]
const c Info[] = [ { 2 } ]
refer immut immut a = v /* 仮にこれをOKとした場合 */
a = c /* v = cが行われてしまう! */
v.m = 9 /* c.mも変更されてしまう! */
上記の「refer immut immut a = v」はRarakuでは許可されませんが、仮にこれが許可されたとすると、 すぐ後の再代入「a = c」が問題となります。 a は refer文で宣言していますから、この再代入「a = c」は再代入「v = c」と同じことです。 本来、再代入「v = c」は許可されないはずですが、refer文で宣言した a を介してこのようなことが可能になってしまいます。 これは例えば、次にある「v.m = 9」などで、本来変更されるはずのない c.m が変更されてしまうことになります。
cが「const c = v」などのように擬似定数として宣言された場合であるならばともかく、 上記では「const c = Info[] = [ { 2 } ]」と完全定数として宣言されていますので、 このcの中身が意図せず変更されるような可能性は排除しなければなりません。 そのため、refer文の左辺と右辺のダブルモディファイアを一致させる必要が生じるわけです。
Close
参考: 第1モディファイアにconstが指定できない理由
refer文において、左辺や右辺の第1モディファイアにconstを指定することは許可されません。
これはreferと第1モディファイアのconstが意味的に競合するからです。
わざわざrefer文を使うということは、0次要素への再代入を想定しているはずですが、 第1モディファイアがconstである時点で0次要素への「再代入禁止」が規定されます。 0次要素へ再代入ができないということは、例えばもし仮にrefer const variaと指定できたところで、 それは結局const variaと実質的に同じことになります。
Close
わざわざrefer文を使うということは、0次要素への再代入を想定しているはずですが、 第1モディファイアがconstである時点で0次要素への「再代入禁止」が規定されます。 0次要素へ再代入ができないということは、例えばもし仮にrefer const variaと指定できたところで、 それは結局const variaと実質的に同じことになります。
Close
関数の仮引数のダブルモディファイア
今まで関数の仮引数には多くとも1つのモディファイアを指定してきましたが、 実はこれにはモディファイアを2つ指定することもでき、これを(仮引数の)ダブルモディファイアと呼びます。 これは「第1モディファイア 第2モディファイア 識別子 型名」といった書式になります。 例えば以下の通りです。
struct Info {
m int
}
function func( varia const p Info[] ){ /* 仮引数pにダブルモディファイア */
varia v Info[] = [ { 4 }, ]
p = v /* OK */
//p[0] = v[0] /* compile error */
//p[0].m = 9 /* compile error */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p に「varia const」といったダブルモディファイアを指定しています。 この場合、p はあたかもvaria文でconstモディファイアを指定したもので宣言されたかのような制限を関数内で受けます。 すなわち再代入「p = v」は(pがvaria受容性より)許可されますが、 再代入「p[0] = v[0]」、「p[0].m = 9」は禁止されます(p[0]、p[0].mはconstですので「再代入禁止」となります)。
指定可能な実引数は、対応する仮引数がその実引数で初期化可能かどうかで判定します。 例えば上記では p はvaria const、a はvaria variaですので、varia constがvaria variaで初期化可能かどうかで判定します。 この初期化は可能ですので、a は指定可能となります。
別の例も見てみましょう。
struct Info {
m int
}
function func( varia varia p Info[] ){ /* 仮引数pにダブルモディファイア */
varia v Info[] = [ { 4 }, ]
p = v /* OK */
p[0] = v[0] /* OK */
p[0].m = 9 /* OK */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p がvaria variaのパターンです。 この場合、p は varia varia としての制限を関数内で受けます。 すなわち再代入「p = v」、「p[0] = v[0]」、「p[0].m = 9」は許可されます。
仮引数で単に「varia」と記述した場合、それは「varia varia」と同じです。
つまり通常「varia varia」記述する必要はなく、その意図では単に「varia」と記述すればよいことになります。
別の例も見てみましょう。
struct Info {
m int
}
function func( const varia p Info[] ){ /* 仮引数pにダブルモディファイア */
varia v Info[] = [ { 4 }, ]
//p = v /* compile error. */
//p[0] = v[0] /* compile error. */
p[0].m = 9 /* OK */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p がconst variaのパターンです。 この場合、p は const varia としての制限を関数内で受けます。 すなわち再代入「p = v」、「p[0] = v[0]」が禁止されます。 「p[0].m = 9」については、p[0]は「集成体不変」ではなく、さらにp[0].mが「varia受容性」ですので許可されます。 実引数 a はvaria variaですので指定可能です。
別の例も見てみましょう。
struct Info {
m int
}
function func( const const p Info[] ){ /* 仮引数pにダブルモディファイア */
varia v Info[] = [ { 4 }, ]
//p = v /* compile error. */
//p[0] = v[0] /* compile error. */
//p[0].m = 9 /* compile error. */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p がconst constのパターンです。 この場合、p は const const としての制限を関数内で受けます。 すなわちすべての再代入「p = v」、「p[0] = v[0]」、「p[0].m = 9」が禁止されます。
仮引数で単に「const」と記述した場合、それは「const const」と同じです。
つまり通常「const const」記述する必要はなく、その意図では単に「const」と記述すればよいことになります。
さらに言えば、関数の仮引数においてモディファイアを何も指定しない場合、デフォルトでそれは「const」となります。
つまり実はモディファイアを何も指定しなかった場合、それは結局「const const」と同じ意味になるということです。
Rarakuではこのように最も制限の厳しい「const const」が仮引数のデフォルトとなっています。
別の例も見てみましょう。
struct Info {
m int
}
function func( tight immut p Info[] ){ /* 仮引数pにダブルモディファイア */
tight t Info[] = [ { 4 }, ]
p = t /* OK */
//p[0] = t[0] /* compile error */
//Rrk_push_bk( p, t[0] ) /* compile error. */
//p[0].m = 9 /* compile error. */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p がtight immutのパターンです。 この場合、p は tight immut としての制限を関数内で受けます。 すなわち再代入「p[0] = t[0]」は(p[0]がimmut受容性ですので)許可されません。 また関数呼び出し「Rrk_push_bk( p, t[0] )」は(pが要素数一定ですので)許可されません。 また「p[0].m = 9」は、「p[0]」が集成体不変ですので許可されません。
仮引数で単に「tight」と記述した場合、それは「tight tight」と同じです。
つまり通常「tight tight」記述する必要はなく、その意図では単に「tight」と記述すればよいことになります。
別の例も見てみましょう。
struct Info {
m int
}
function func( immut varia p Info[] ){ /* 仮引数pにダブルモディファイア */
varia v Info[] = [ { 4 }, ]
//p = v /* compile error */
//p[0] = v[0] /* compile error */
//Rrk_push_bk( p, v[0] ) /* compile error */
p[0].m = 9 /* OK */
}
varia a Info[] = [ { 1 }, { 2 }, ]
func( a )
上記は仮引数 p がimmut variaのパターンです。 この場合、p は immut varia としての制限を関数内で受けます。 すなわち再代入「p = v」は(pがimmut受容性より)許可されません。 また「p[0] = v[0]」は(pが集成体不変より)許可されません。 また関数呼び出し「Rrk_push_bk( p, v[0] )」は(pが要素数一定より)許可されません。 また「p[0].m = 9」は、「p[0].m」がvaria受容性より許可されます。
仮引数で単に「immut」と記述した場合、それは「immut immut」と同じです。
つまり通常「immut immut」記述する必要はなく、その意図では単に「immut」と記述すればよいことになります。
次の例はreferを使ったものです。
struct Info {
m int
}
function func( refer immut immut p Info[] ){ /* 仮引数pにダブルモディファイア */
immut i Info[] = [ { 4 }, ]
p = i /* OK */
//p[0] = i[0] /* compile error */
//p[0].m = 9 /* compile error. */
}
immut a Info[] = [ { 1 }, { 2 }, ]
func( a ) /* OK */
上記は仮引数 p に「refer immut immut」を指定しています。 この場合、p は immut immut としての制限を関数内で受けます。
上記では再代入「p = i」は許可されます。 また勿論、referが指定されていますので「p = i」という再代入は関数外にも反映されます。
既に述べた通り、指定可能な実引数は、対応する仮引数がその実引数で初期化可能かどうかで判定します。 ただし今回はreferが付いていますので、その初期化はrefer文と同様の注意が必要です。 例えば仮引数が集成体の場合、実引数のダブルモディファイアは仮引数のダブルモディファイアと一致していなければなりません。 上記では、実引数aはimmut immut、仮引数pもimmut immutですので問題ありません。
refer付きの仮引数において、「refer varia」は「refer varia varia」と同じであり、
「refer tight」は「refer tight tight」と同じであり、
「refer immut」は「refer immut immut」と同じです。
尚、仮引数の場合も、referの第1モディファイアにconstを指定することはできません。 例えば「refer const」や「refer const varia」などといった指定はできません。
尚、仮引数の場合も、referの第1モディファイアにconstを指定することはできません。 例えば「refer const」や「refer const varia」などといった指定はできません。
仮引数で単に「refer」と記述した場合、それは「refer immut immut」と同じです。
つまり通常「refer immut immut」記述する必要はなく、その意図では単に「refer」と記述すればよいことになります。
これはrefer文でモディファイアを一つも付けない場合の挙動とは異なることにご注意ください。 refer文でモディファイアを一つも付けない場合、モディファイア推論により右辺のモディファイアと完全に一致します (つまりrefer文の場合は(refer付きの仮引数とは異なり)refer immut immutの省略形という意味にはなりません)。
これまで非集成体の仮引数において、(例えばswap関数の実装などで)単に「refer」と指定してきましたが、
これが機能したのは非集成体では、「refer immut immut」で十分であるためです。
一方、集成体で「refer」を指定した場合、モディファイアに気をつける必要があります。
これはrefer文でモディファイアを一つも付けない場合の挙動とは異なることにご注意ください。 refer文でモディファイアを一つも付けない場合、モディファイア推論により右辺のモディファイアと完全に一致します (つまりrefer文の場合は(refer付きの仮引数とは異なり)refer immut immutの省略形という意味にはなりません)。
関数の戻り値の型のダブルモディファイア
関数の戻り値の型にもダブルモディファイアを指定することができます。 これは「第1モディファイア 第2モディファイア 型名」といった書式になります。 考え方は仮引数のダブルモディファイアと全く同じです。 このときreturn文で指定された式が(初期化における)右辺、戻り値の型で指定された方が左辺とみなして考えましょう。 例えば以下の通りです。
struct Info {
m int
}
function func() tight varia Info[] { /* 戻り値の型にダブルモディファイア */
varia a Info[] = [ { 1 }, { 2 }, ]
return a
}
func()[0].m = 9
上記ではreturn文に指定されている aはvaria varia、戻り値の型はtight variaとなっており、 この場合、tight varia を varia varia で初期化可能かどうかで考えます。 tight varia を varia varia で初期化することは可能ですので、上記のケースも許可されます。
上記では、関数funcの呼び出し側において若干複雑な記述をしています。 この「func()[0].m = 9」は、その戻り値を配列とみなし、かつその要素にアクセスするような処理に相当しますが、 戻り値の型での第2モディファイアはこのような場合に意味を持ちます。 例えば上記の例では(戻り値の型での)第2モディファイアはvariaになっており、 戻り値の配列の要素「func()[0].m」が「varia受容性」により規定されます。 上記の例ではそれにint型を再代入していますのでこれは許可されます。
戻り値の場合、デフォルトのモディファイアはconstではなくvariaになっていることにも注意してください。
すなわち戻り値にモディファイアを何も指定しない場合、それは「varia varia」と指定したのと同じになります。
また、単に戻り値にvariaモディファイアを一つ付けた場合は「varia varia」と同じであり、 単に戻り値にtightモディファイアを一つ付けた場合は「tight tight」と同じであり、 単に戻り値にimmutモディファイアを一つ付けた場合は「immut immut」と同じであり、 単に戻り値にconstモディファイアを一つ付けた場合は「const const」と同じです。
また、単に戻り値にvariaモディファイアを一つ付けた場合は「varia varia」と同じであり、 単に戻り値にtightモディファイアを一つ付けた場合は「tight tight」と同じであり、 単に戻り値にimmutモディファイアを一つ付けた場合は「immut immut」と同じであり、 単に戻り値にconstモディファイアを一つ付けた場合は「const const」と同じです。
関数の戻り値にrefer指定した場合も仮引数と同様に考えることができます。 例えば以下の通りです。
struct Info {
m int
}
function func() refer tight varia Info[] { /* 戻り値の型にreferとダブルモディファイア */
tight varia a Info[] = [ { 1 }, { 2 }, ]
return a
}
varia x Info[] = [ { 3 }, { 4 }, ]
func() = x
上記ではreturn文に指定されている aはtight varia、戻り値の型はtight variaとなっており、 この場合も、tight varia を tight varia で初期化可能かどうかで考えます。 referで集成体の場合、その初期化では左辺と右辺のダブルモディファイアは一致していなければならないのでした。 よって上記の a はその条件を満たしますので許可されます。
一方、この関数を呼び出している側では「func() = x」のように記述してあります。 ここで行われているのは関数の戻り値への再代入であり、初期化ではありません。 代入先であるfunc()とその右辺であるxは通常のダブルモディファイアのアサイナビリティを満たせばよく、 上記ではfunc()はtight varia、xはvaria variaですから、この再代入は許可されます。
refer付きの戻り値の型において、「refer varia」は「refer varia varia」と同じであり、
「refer tight」は「refer tight tight」と同じであり、
「refer immut」は「refer immut immut」と同じです。
尚、戻り値の型の場合も、referの第1モディファイアにconstを指定することはできません。 例えば「refer const」や「refer const varia」などといった指定はできません。
尚、戻り値の型の場合も、referの第1モディファイアにconstを指定することはできません。 例えば「refer const」や「refer const varia」などといった指定はできません。
refer付きの戻り値においてモディファイアを何も指定しない場合、それは「refer immut immut」と指定したのと同じになります
(「refer varia varia」ではありませんのでご注意ください)。
これはrefer付きの仮引数でモディファイアを何も指定しなかった場合の仕様に合わせるためにこのように規定しています。
構造体のメンバのモディファイア
ダブルモディファイアとは少し違いますが、モディファイアに関するその他のトピックスとして構造体のメンバのモディファイアについても ここで説明しておきます。 通常、構造体のメンバにはモディファイアを付記しませんが、これにモディファイアを付記することもできます。 まずは以下をご覧下さい。
struct Info {
m int
}
struct Info2 {
n Info
}
varia s Info2
const c Info = { 10 }
s.n = c /* OK */
上記において、最後の再代入「s.n = c」は許可されません。 sはvaria variaであり、よって第2モディファイアvariaより1次要素 n も「varia受容性」となり、 一方で右辺のcはconstですのでアサイナビリティの規則を満たしません。
しかし、構造体のメンバにimmutを指定することによってこの n における「varia受容性」を「immut受容性」へ強制的に上書きすることができます。 例えば以下の通りです。
struct Info {
m int
}
struct Info2 {
immut n Info
}
varia s Info2
const c Info = { 10 }
s.n = c /* OK */
上記では「s.n = c」では const から immutへの再代入となり、これが許可されます。
ただしこのような構造体のメンバに指定されたモディファイアによる上書きは、 その親となる構造体由来の制限よりもconst側に近い指定になる場合にのみ有効です。 例えば上記の場合、元々構造体 s のダブルモディファイアによって n は「varia受容性」となっていましたが、 それよりもconst側に近い「immut受容性」への指定ですので、このような上書きは有効になります。
一方、その逆の状況では、このような上書きは行われず、構造体のメンバに指定されたモディファイアの方が単に無視されます。 例えば以下をご覧下さい。
struct Info {
m int
}
struct Info2 {
varia n Info
}
immut s Info2
varia v Info = { 10 }
s.n = v /* NG: compile error */
上記において、最後の再代入「s.n = v」は許可されません。 sはimmut immutであり、よって第2モディファイアimmutより1次要素 n も「immut受容性」となります。 一方、Info2のメンバ n には、今回 variaが指定されてはいますが、immutの方がvariaよりもconstに近い指定となるため、 この場合immutの方が優先され、メンバ n に指定されたvariaは単に無視されます。 結局「s.n = v」では varia から immutへの再代入となり、これは許可されません。
ダブルモディファイアのまとめ
構造体の配列や配列の配列といったように、要素次数が大きな複雑な集成体を扱う場合、 集成体自身やその要素のそれぞれのアクセスや再代入に関して、細かい制限をしたい場合があります。 そのような場合ダブルモディファイアを使いましょう。
目次に戻る
final文
varia/tightで宣言された変数を途中からconstとして扱う
プログラミングにおいて、変数の値の変更はバグが最も発生しやすいタイミングの一つです。 ですから一般的には極力そのような変更に制限をかける方が望ましいといわれています。 またコードを読む際においても、変数の値が変更される可能性があることを考慮に入れながら読むのと そうではない場合では、後者の方がずっと労力が軽減されます。 その観点から言えば変数として宣言するのは極力避け、代わりにconst文で定数にしてしまう方が理想的であるわけです。
もちろん実際にはvaria文で変数としなければならないことも往々にしてあります。
極端にconst化を押し進めるあまり、コードが(多少ならともかく)酷く複雑化しては本末転倒です。
そのあたりのバランスには常に気を配る必要があります。
ところで実際にはその変数の値が変更されるタイミングがコードの最初の部分だけであり、それ以降は(定数のように)変更されることはないといった いわばほとんど定数のような変数もあります。 このようなものも一応最初に値の変更が伴う以上、varia文で宣言する必要はあります。 しかし、変更する必要が明らかになくなった時点でconstとして扱って欲しいと思うのも自然でしょう。
Rarakuではこのようなことを実現するためにfinal文を使用することができます。 これは「final 識別子のリスト(カンマ区切り)」といった書式になります。 例えば以下のようになります。
const iary int[] = [ 1, 2, 3, 4, 5 ]
varia sum = 0
foreach i : iary {
sum += i
}
final sum /* sum はこれ以降変更禁止(つまりconstとして扱う) */
/* これ以降もsumをリードオンリーで参照するだけのコードが長々と続く */
上の例では、final文が字面に登場するより前まではsumは純然たる(variaで宣言された)変数です。 しかしfinal文でこれを指定した瞬間、それ以降は(あたかもconst文で宣言された)定数として扱われます。 つまりそれ以降で、もし(なんらかのプログラミングミス等により)sumを変更するような処理を書いてしまった場合、 コンパイルエラーを表示してくれるようになります。
ただしfinal文で指定する識別子は、既にvaria文で宣言済みのものでなければなりません。 宣言されていない識別子を指定した場合コンパイルエラーとなります。
final x /* コンパイルエラー: x はまだ(varia文で)宣言されていない */
Javaにもfinalというキーワードがありますが、Rarakuのfinal文はそれとは働きが異なります。
Javaでのfinalは(メソッドの継承等の話を考えなければ)基本的に定数を宣言する類の働きをし、あくまで実体の宣言と同時に用いられます。
一方、Rarakuのfinal文は実体の宣言と同時ではなく、既に宣言された変数を後から別の文として強制的に定数とみなす働きをします。
よって例えばfinal文単独で定数を宣言するようなことはできません。
このように既に宣言された変数を途中からfinal文でconstとして扱うことを、Rarakuではfinalizeする(確定化する)と呼ぶこともあります。
他のプログラミング言語では、このような処理をfreezeする(凍結する)と呼ぶこともあります。
例えばJavascriptでは、文字通りfreezeという名前のメソッドでこのような処理が実現可能です
(ただしJavascriptの場合、この凍結は動的なものです)。
またRustではシャドーイング、すなわちイミュータブルに再宣言することによってこれを実現しているようです。
final文に指定する「識別子のリスト」ではカンマ区切りで複数の識別子を一度に指定できます。 例えば以下の通りです。
varia i = 10
varia j = 10
final i, j
以下のように最後の識別子の直後にもカンマを置くこともできますがその場合は文末の「;」が必須となります。
varia i = 10
varia j = 10
final
i,
j,
; /* この場合「;」が必須 */
元の宣言がvariaではなくtightで宣言された配列であった場合も同様です。 例えば以下の通りです。
tight ary int[] = [ 10, 20, 30 ]
ary[0] = 100
ary[1] = 200
ary[2] = 300
final ary
/* これ以降 ary は tight ではなく const (値の変更も不可となる).
variaで宣言された変数を途中からtightとして扱う
これまではfinal文でvariaをconst化する例を見ましたが、これをtight化することもできます。 そのためには、final文で指定する識別子の前にtightを指定します。 例えば以下の通りです。
varia ary int[] = []
Rrk_push_bk( ary, 10 )
Rrk_push_bk( ary, 20 )
Rrk_push_bk( ary, 30 )
final tight ary
/* これ以降 ary は tight として扱われる(要素の値の変更は依然として可能だが、要素数の変更は禁止される) */
あまりないことかもしれませんが、一つのfinal文で複数の識別子を同時にtightにしたい場合、 それぞれの識別子の前にtightが必要となります。
varia ary1 int[] = []
Rrk_push_bk( ary1, 10 )
Rrk_push_bk( ary1, 20 )
Rrk_push_bk( ary1, 30 )
varia ary2 int[] = []
Rrk_push_bk( ary2, 10 )
Rrk_push_bk( ary2, 20 )
Rrk_push_bk( ary2, 30 )
final tight ary1, tight ary2
/* これ以降 ary1, ary2 は tight として扱われる */
上記を仮に以下のように書いた場合、tightのついてない2番目の識別子はconst化されます。
varia ary1 int[] = []
Rrk_push_bk( ary1, 10 )
Rrk_push_bk( ary1, 20 )
Rrk_push_bk( ary1, 30 )
varia ary2 int[] = []
Rrk_push_bk( ary2, 10 )
Rrk_push_bk( ary2, 20 )
Rrk_push_bk( ary2, 30 )
final tight ary1, ary2 /* ary1はtight化され、ary2はconst化される */
単に「final ary」と書いた場合それは const 化されるわけですが、これを「final const ary」と書いても構いません。
例えば以下の通りです。
実のところ「final ary」は「final const ary」の省略形です。 そのためこれがconst化されるわけです。
一度に複数指定する例も挙げておきます。
上記ではary1, ary2, ary3, ary4がそれぞれ指定されたモディファイアでtight化、const化されます。 そしてこれは以下のように書いたのと全く同じです。
final文ではこのようにconstモディファイアについては省略できます (ただしこのようにtightとconstを混在して書く場合はconstも明示的に付けた方が見やすいと思います)。
varia ary int[] = []
final const ary /* final ary と書いたのと全く同じ */
実のところ「final ary」は「final const ary」の省略形です。 そのためこれがconst化されるわけです。
一度に複数指定する例も挙げておきます。
varia ary1 int[] = []
varia ary2 int[] = []
varia ary3 int[] = []
varia ary4 int[] = []
final
tight ary1,
const ary2,
tight ary3,
const ary4,
;
上記ではary1, ary2, ary3, ary4がそれぞれ指定されたモディファイアでtight化、const化されます。 そしてこれは以下のように書いたのと全く同じです。
varia ary1 int[] = []
varia ary2 int[] = []
varia ary3 int[] = []
varia ary4 int[] = []
final
tight ary1,
ary2,
tight ary3,
ary4,
;
final文ではこのようにconstモディファイアについては省略できます (ただしこのようにtightとconstを混在して書く場合はconstも明示的に付けた方が見やすいと思います)。
stringで宣言された変数を途中からconststrとして扱う
final文ではstring型で宣言された変数をconststr化するようなこともできます。 この書式はconst/tightの場合とは異なり、「final 識別子 conststr」という書式になります。 「conststr」の部分が識別子の後に来ることに注意してください。 例えば以下の通りです。
varia str string
str &= "hello" /* OK: strはstring型 */
final str conststr /* strをconststr型へ変更 */
str &= "world" /* コンパイルエラー: final文によりstrはconststr型に変更された */
上記では途中でfinal文によりstrがconststr型に変更されました。 文字列連結演算子「&=」はstring型に対しては適用できますが、conststr型に対しては適用できません。 そのため、「str &= "world"」の部分は狙い通りコンパイルエラーとなるわけです。
「final str conststr」と書いた場合、これは実際には「final const str conststr」の省略形となります。 よってこの場合、「&=」だけでなく以下のように「=」による代入も出来なくなります。
varia str string
str = "hello" /* OK: strはvaria */
final str conststr /* strはconstにも変更されている */
str = "world" /* コンパイルエラー: final文によりstrはconstに変更された */
あまりないことかもしれませんが、conststrに変更したいが(「=」による代入がまだ必要なので)variaのままでいて欲しい場合 「final varia str conststr」と書くことも一応できます。 例えば以下の通りです。
varia str string
str = "hello" /* OK: strはvaria */
final varia str conststr /* strをvariaなconststrに変更 */
str = "world" /* OK: strはvariaなままなのでこの代入は許可される */
final文によるconststr指定では、与える識別子はstring型かconststr型でなければなりません (それ以外の型の識別子を与えることはできません)。 ただし識別子の型がconststr型の場合、元々conststr型ですので特に何も行われないことになります。
final文の効果を一時的なものとしたい場合
前項ではほとんど定数であるような変数を宣言する場合、まずはvaria文として宣言し、値の変更が不要になった時点で(final文で)finalizeすると述べました。 しかし場合によっては、コードの最後の方でも値の変更を行いたいかもしれません。 例えば以下のような場合を考えます。
function calc() int { .. 1 + 1 }
varia val = calc()
/* このタイミングでfinalize化したい */
/***
* 一旦calc関数で返した値のままで何かの処理する.
* この間は val を const として扱いたい.
*/
if val == 2 {
Rrk_print( "OK"\n )
}
/* しかし最後にだけ、値を本当に変更する必要がある場合 */
if val < 0 {
val = 0
}
上の例で、仮にvalの初期化の直後にfinalizeしてしまうと、コードの一番最後で変更が伴うためコンパイルエラーとなってしまいます。 つまり final文の効果を一時的なものとしたいわけですが、このような場合ブロックを使ってそれが有効な範囲を限定できます。 例えば以下の通りです。
function calc() int { .. 1 + 1 }
varia val = calc()
{
/* この時点ではまだ val は変更可能 */
/* これ以降valが変更不可(変更しようとする処理に対してコンパイルエラーを出す). */
/* ただしその効果はこのブロックが終了するまで */
final val
if val == 2 {
/* 配下のブロック内でもfinal valの効果は持続 */
Rrk_print( "OK"\n )
}
/* ここでブロックが終わり、final valの効果も終了. */
}
/* final val を宣言したブロックの外なので val は変更可能. */
/* 最後にだけ、値を本当に変更する必要がある場合 */
if val < 0 {
val = 0
}
varia文で宣言した識別子以外のものをfinalizeした場合
final文は基本的にはvaria文で宣言した識別子をfinalizeするためのものですが、 const文やtight文やrefer文で宣言した識別子を指定することも一応許可されます。
const文で宣言した識別子をfinal文で指定した場合、元々constであるためこれは何の意味も持ちません。 tight文で宣言した識別子をfinal文で指定した場合、要素の値の変更も禁止されることになります。 refer文で宣言した識別子をfinal文で指定した場合は、varia文と同様にfinalizeされます。 つまり(final文が有効な範囲でですが)その識別子への代入が許可されなくなり、要素の値の変更も禁止されます。
その他、関数の呼び出しや構造体のドット演算子によるメンバへのアクセス、 配列のインデックスによる要素のアクセスなどをfinal文に指定した場合はコンパイルエラーとなります。
final文のまとめ
プログラミングにおいては、変数の値が変更されるタイミングがコードの最初の部分だけであり、 それ以降は(定数のように)変更されることはないといったほとんど定数のような変数があります。 しかも定数のように変更されることはない区間が後ろに長く続くとしましょう。 そのような変数は、値を変更する必要が明らかになくなった時点でfinal文で宣言しましょう。 コンパイラはfinal文以降、この変数をあたかもconst宣言された定数のように扱います。
またこのようにするとコードを読む際においても、final文以降では変数の値の変遷を考える必要がなくなる分、ずっと労力が軽減されます。
目次に戻る
functype文とコールバック関数
関数型とfunctype文
Rarakuでは関数そのものを変数として格納することができます。 このときこの変数の型は関数型と呼ばれ、型そのものはfunctype文により定義します。
functype文の書式は「functype 関数型名 ( 引数の指定 ) 戻り値」の指定になり、 あたかも関数の宣言において function キーワードだけを functype キーワードに置き換えたような形式になります。 ただしfunctypeの場合はその後に続けてブロックを記述することはありません。
functypeはfunctionとtypeを合体させたようなキーワードになっています。
何と発音するかですが「ファンクタイプ」でいいんじゃないかと思います。
例えば以下の例をご覧ください。
/* 関数型FuncT_BinOpの定義 */
functype FuncT_BinOp( refer x int, refer y int ) void
/* 関数swapの定義(FuncT_BinOp型と型が一致する) */
function swap( refer x int, refer y int ) void
{
const tmp int = x
x = y
y = tmp
}
function dummy1( refer x int, refer y conststr ) void
{
}
function dummy2( refer x int, refer y int ) int
{
return 0
}
varia func_var FuncT_BinOp = swap /* FuncT_BinOp型変数func_varを関数swapで初期化 */
varia x = 10
varia y = 20
Rrk_print( "before : x=" x ", y=" y "\n" )
func_var( x, y ) /* 関数型の変数を介して関数(swap)を呼び出す。*/
Rrk_print( "after : x=" x ", y=" y "\n" )
この例では、最初にfunctype文によりFuncT_BinOpという名前の新しい関数型が定義されています (この例では関数型であることがわかりやすいようにFuncT_というプレフィックスをつけていますが、 名前は識別子の条件さえ満たすならどのようなものでも可能です)。
関数型変数に格納できる関数は、その引数の型と戻り値の型がすべてマッチしているものだけになります。 例えば上の例での関数型FuncT_BinOpは、二つのrefer int型の引数を持ち、戻り値を返さないような関数のみにマッチします。 swap関数はこの型にマッチしています。 しかしdummy1は引数の型が一致しないためこの型にマッチしません。 またdummy2は戻り値の型が一致しないためこれもマッチしません。
関数型変数も通常の変数と同様にvaria文で宣言することができます。 上の例では func_var という名前の変数を宣言しており、またこれに代入可能な関数はswapだけになります。
関数型変数を使ってその中身に該当する関数を呼び出すことができます。 上の例の「func_var( x, y )」では、変数func_varをあたかも関数呼び出しのようなフォームで記述していますが、 これによりfunc_varに格納された関数の実体(この例の場合swap)が呼び出されます。
これを実行した結果を以下に示します。
before : x=10, y=20
after : x=20, y=10
関数型のモディファイアのアサイナビリティ
前項でも述べた通り、関数型変数の初期化や再代入は、 与えられた右辺と左辺の仮引数と戻り値の型がすべて一致している場合に許可されます。
では型はすべて一致し、モディファイアだけが異なる関数型変数が右辺と左辺に与えられた場合はどうでしょうか? これは許可される場合と許可されない場合があり、特に仮引数の型が集成体(構造体や配列)の場合に注意が必要です。 まずは以下をご覧下さい。
struct Info {
m int
}
functype FuncT1( varia x Info ) void
functype FuncT2( const x Info ) void
function func1( varia x Info ) void {
}
function func2( const x Info ) void {
}
varia f1 FuncT1 = func1
varia f2 FuncT2 = func2
varia f1 = f2 /* OK */
//varia f2 = f1 /* NG: compile error */
上記では二つの関数型変数f1、f2を宣言し、それぞれを他方へ再代入するといった例になります。 f1とf2はどちらもInfo構造体を仮引数に持つ関数型変数ですが、そのモディファイアのみが異なります。
まず再代入「f1 = f2」についてはこの場合、許可されます。 f1の仮引数のモディファイアはvaria、f2の仮引数のモディファイアはconstですから、 関数型の仮引数だけを見れば、これはconstからvariaへといった方向になっています。
次に再代入「f2 = f1」についてはこの場合、許可されません(コンパイルエラーとなります)。 f2の仮引数のモディファイアはconst、f1の仮引数のモディファイアはvariaですから、 関数型の仮引数だけを見れば、これはvariaからconstへといった方向となっています。
ここで集成体のアサイナビリティを今一度思い出しますと、constからvariaへの初期化/再代入は許可されなかったはずです。 そのため、上記は一見するとこのアサイナビリティに反しているかのように見えます。 実は関数型変数のアサイナビリティの場合、仮引数に関してはある意味基本規則が逆転した形になり、 (仮引数だけを見た場合に)constからvariaへの方向はOKですが、variaからconstへの方向はNGとなります。 もう少し一般的に言えば、(仮引数だけを見た場合に)制限の強い方から弱い方への(または制限が全く同じとなるような) 関数型の代入はOKですが、その逆はNGということです。
このような仕様になっている理由は、代入先の関数型引数をこの後呼び出すことを考えると理解できます。
f1の呼び出しは、実引数で与えた変数の値が「変化する/しない」のいずれの状況も了承した呼び出しとなります。 よってこのf1にf2(引数の値が変化しない関数)を代入したとしても特に問題はありません。
一方、f2の呼び出しは、実引数で与えた変数の値が「変化しない」ことを前提とした呼び出しになります。 よって仮にこのf2にf1(引数の値が変化し得る関数)を代入可能とした場合、f2側にある前提が崩れることになり問題となります。
f1の呼び出しは、実引数で与えた変数の値が「変化する/しない」のいずれの状況も了承した呼び出しとなります。 よってこのf1にf2(引数の値が変化しない関数)を代入したとしても特に問題はありません。
一方、f2の呼び出しは、実引数で与えた変数の値が「変化しない」ことを前提とした呼び出しになります。 よって仮にこのf2にf1(引数の値が変化し得る関数)を代入可能とした場合、f2側にある前提が崩れることになり問題となります。
次に仮引数のモディファイアは全て同じで、戻り値のモディファイアだけが異なる場合を考えましょう。 以下をご覧下さい。
struct Info {
m int
}
functype FuncT1( varia x Info ) Info
functype FuncT2( varia x Info ) const Info
function func1( varia x Info ) Info {
return x
}
function func2( varia x Info ) const Info {
return x
}
varia f1 FuncT1 = func1
varia f2 FuncT2 = func2
//f1 = f2 /* NG: compile error */
f2 = f1 /* OK */
今回も二つの関数型変数f1、f2を宣言し、それぞれを他方へ再代入するといった例になります。 f1とf2は戻り値のモディファイアのみが異なり、f1の方はvaria(戻り値の型のモディファイアが省略された場合のデフォルト)、 f2の方はconstとなっています。
まず再代入「f1 = f2」については(仮引数の場合とは逆に)この場合、許可されません(コンパイルエラーとなります)。 一方、再代入「f2 = f1」についてはこの場合、許可されます。 つまり、戻り値のモディファイアについては、通常の集成体のアサイナビリティと方向性が一致しているといえます。
このような仕様になっている理由は、やはり代入先の関数型引数をこの後呼び出すことを考えると理解できます。
f1の呼び出しは、戻り値で与えた変数を(その外側で)variaとして宣言された変数へ代入することを許可します。 よってこのf1にf2(戻り値をconstとして格納することしか許可しない関数)を代入すると、 f2側の戻り値の実体がvaria変数へ格納される可能性を認めてしまいは問題になります。
一方、f2の呼び出しは、戻り値で与えた変数を(その外側で)constとして宣言された変数へ代入すること以外は許可しません。 よってこのf2にf1(戻り値をconst/variaのいずれでも格納可能な関数)を代入しても f1側の前提は崩れず特に問題はないことになります。
f1の呼び出しは、戻り値で与えた変数を(その外側で)variaとして宣言された変数へ代入することを許可します。 よってこのf1にf2(戻り値をconstとして格納することしか許可しない関数)を代入すると、 f2側の戻り値の実体がvaria変数へ格納される可能性を認めてしまいは問題になります。
一方、f2の呼び出しは、戻り値で与えた変数を(その外側で)constとして宣言された変数へ代入すること以外は許可しません。 よってこのf2にf1(戻り値をconst/variaのいずれでも格納可能な関数)を代入しても f1側の前提は崩れず特に問題はないことになります。
尚、上記のようなケースではfunctype文を省略して書くこともできます。 例えば以下の通りです。
struct Info {
m int
}
function func1( varia x Info ) Info {
return x
}
function func2( varia x Info ) const Info {
return x
}
varia f1 = func1
varia f2 = func2
//f1 = f2 /* NG: compile error */
f2 = f1 /* OK */
「varia f1 FuncT1 = func1」と書く替わりに、単に「varia f1 = func1」と書いても、 右辺のfunc1より左辺のf1の型は推論できます。 この場合、「functype FuncT1( varia x Info ) Info」という宣言は存在しませんが、 それに相当する(無名の)型として型推論される形になります。
関数型のrefer指定のアサイナビリティ
型はすべて一致し、モディファイアにおけるアサイナビリティも問題ない上で、 refer指定の有無だけが異なる関数型変数が右辺と左辺に与えられた場合はどうでしょうか? この場合もモディファイアと同様、許可される場合と許可されない場合があります。 まずは以下をご覧下さい。
function func1( refer x int ) void {
}
function func2( x int ) void {
}
varia f1 = func1
varia f2 = func2
f1 = f2 /* OK */
//f2 = f1 /* NG: compile error */
今回も二つの関数型変数f1、f2を宣言し、それぞれを他方へ再代入するといった例になります。 f1とf2はどちらもint型を仮引数に持つ関数型変数であり、そのモディファイアも同じです。 f1の方にはrefer指定があり、f2の方にはそれがない点のみが異なります。
まず再代入「f1 = f2」についてはこの場合、許可されます。 一方、再代入「f2 = f1」についてはこの場合、許可されません(コンパイルエラーとなります)。 つまり仮引数にrefer指定のない関数型から、その指定がある関数型への代入はOKですが、 その逆はNGということです。
このような仕様になっている理由は、やはり代入先の関数型引数をこの後呼び出すことを考えると理解できます。
f1の呼び出しは、refer指定があるため、実引数で与えた変数の値が「変化する/しない」のいずれの状況も了承した呼び出しとなります。 よってこのf1にf2(refer指定がないため、実引数の値が変化する可能性はない関数)を代入したとしても特に問題はありません。
一方、f2の呼び出しは、(refer指定がないため)実引数で与えた変数の値が「変化しない」ことを前提とした呼び出しになります。 よって仮にこのf2にf1(refer指定があるため、実引数の値が変化し得る関数)を代入可能とした場合、 f2側にある前提が崩れることになり問題となります。
f1の呼び出しは、refer指定があるため、実引数で与えた変数の値が「変化する/しない」のいずれの状況も了承した呼び出しとなります。 よってこのf1にf2(refer指定がないため、実引数の値が変化する可能性はない関数)を代入したとしても特に問題はありません。
一方、f2の呼び出しは、(refer指定がないため)実引数で与えた変数の値が「変化しない」ことを前提とした呼び出しになります。 よって仮にこのf2にf1(refer指定があるため、実引数の値が変化し得る関数)を代入可能とした場合、 f2側にある前提が崩れることになり問題となります。
関数のエイリアス(別名)
const/varia文で型の指定を省略し、かつ右辺に関数を指定して初期化した場合、 右辺に指定した関数型の変数/定数が宣言されたとみなされます。
例えば以下の例をご覧ください(func_varの部分です)。
function swap( refer x int, refer y int ) void
{
const tmp int = x
x = y
y = tmp
}
varia func_var = swap
この例では func_var のvaria文に型は指定されていませんが、右辺に関数swapが指定されているため、 func_var は 関数swapに相当する型(refer int型引数を二つ持ち、戻り値がない関数型)で宣言されたとみなされます (このときswapの関数型に相当する型をfunctype文で明示的に宣言していなくても構いません)。
これを応用してあたかも関数の別名を定義したかのような挙動を実現することもできます。 つまり「const 関数の別名 = 真の関数名」という書式で別名を定義できます。
constの替わりにvariaを使うこともできますが、
関数の実体を途中で変更しない場合はconstを使う方が望ましいでしょう。
例えば以下の例をご覧ください。
import std/str
varia text1 = "Hello world"
varia text2 = "Alias function"
for i:=0u; i<RrkStr_leng(text1); ++i {
varia ch = RrkStr_at( text1, i )
}
Rrk_print( text1 "\n" )
for i:=0u; i<RrkStr_leng(text2); ++i {
varia ch = RrkStr_at( text2, i )
}
Rrk_print( text2 "\n" )
この例では文字列長を得るための標準関数としてRrkStr_leng、文字列内の文字へのアクセス関数としてRrkStr_at、 文字列を出力する関数としてRrk_printを使用しています(単に使用しているだけで特に意味はない例ですが)。 これらに短い別名を与えて使用したものが以下になります。
import std/str
/* 関数のエイリアス(別名)*/
const leng = RrkStr_leng
const at = RrkStr_at
const print = Rrk_print
varia text1 = "Hello world"
varia text2 = "Alias function"
for i:=0u; i<leng(text1); ++i {
varia ch = at( text1, i )
}
print( text1 "\n" )
for i:=0u; i<leng(text2); ++i {
varia ch = at( text2, i )
}
print( text2 "\n" )
全体の行数は増えておりますが、関数のエイリアスを設定している3行は考えないことにしましょう。 長い名前の関数を頻繁に使用する場合はこのようにした方がコードが簡潔になることもあります (もっとも上記の例の場合、最初から「at( text1, i )」よりもさらに短く「text1[i] 」と記述することもできますので、 atについては不要かもしれません)。
Rarakuの標準関数名にはすべてプレフィックスRrkStr_、Rrk_などが付加されています。 これは一つには名前の衝突を避けるため、もう一つはその関数の所属が一目でわかるようにするためです。
とはいえ明らかに名前衝突の心配がない文脈では、このプレフィックスはなくても問題ないでしょう。 そのような場合関数のエイリアスを使うのもよいでしょう。 尚、このエイリアスの実体はあくまで関数型の変数ですから、この変数が有効なスコープ内でのみこの別名が有効になります。
コールバック関数
ここまでの説明だけですと単に直接関数を呼び出すのと比べて関数型変数に何のメリットがあるのか分かりにくいかもしれません。 関数型変数を使うおそらく最大のメリットは、具体的な関数を実装する前の段階で処理を抽象化して記述できることです。
もう一つ大きなメリットとして無名関数を用いてローカル関数を記述できるというものがあります。
ただし無名関数についてはまだ説明していませんので、ここではその説明は割愛します。
無名関数については「無名関数」のセクションで、またローカル関数については「ヘッダファイルとリンケージ」のセクションで詳しく説明します。
以下のコードではAnimalという抽象化された構造体がまず前半にあり、 この構造体は文字列型のメンバnameと関数型のメンバbarkを持ちます。 後半では、その構造体の具体的な実装例として、Dog、Cat、Mouseの3つについて、 それぞれの名前nameと関数barkを記述しています。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action
}
/* これは(Animalを具体的に実装する前の段階で)抽象化して記述できる */
function actionAll( anim_ary Animal[] ) void
{
foreach anim : anim_ary {
Rrk_print( "==== " anim.name " says ====\n" )
Rrk_print( "\t" )
anim.bark()
}
}
function init( varia anim_ary Animal[], anim Animal ) void
{
Rrk_push_bk( anim_ary, anim )
}
/* 以下、具体的な実装 */
varia st_anim_ary Animal[]
function barkDog() void
{
Rrk_print( "Bow wow!\n" )
}
init( st_anim_ary, { "Dog", barkDog } )
function barkCat() void
{
Rrk_print( "Meow meow!\n" )
}
init( st_anim_ary, { "Cat", barkCat } )
function barkMouse() void
{
Rrk_print( "Squeak squeak!\n" )
}
init( st_anim_ary, { "Mouse", barkMouse } )
/* actionAllの実行 */
actionAll( st_anim_ary )
上の例をもう少し詳しく見てみます。 まずFuncT_Action型とそれを使った構造体Animalを宣言した後、 それらの型を使った actionAll 関数を記述しています。 この時点ではまだAnimalの具体的な例はなにも実装されていないということが重要です。
init関数の定義では、Rrk_push_bkにより構造体 anim を、配列anim_aryに追加しています。 このときanim_aryの要素数が変わりますから、仮引数anim_aryの前にはvariaモディファイアが必要になります (何もつけない状態ですとconstモディファイアと同じになりますが、constでは要素数の変更が禁止されるため、 内部でRrk_push_bkを使うことができません)。
具体的な実装はそれより下にあるbarkDog、barkCat、barkMouse関数などが記述されている部分です。 これらの関数はすべてFuncT_Action型とマッチし、構造体 anim の bark メンバに代入可能です。 各実装では構造体 anim の bark メンバに(構造体の初期化子を用いて)これらの関数をセットしています(同時に"Dog"などの名前もセットしています)。
actionAll 関数内の関数呼び出し「anim.bark()」は、実行時にはbarkDog、barkCat、barkMouseを呼び出す側に立ちます。 それにも関わらずコード上では基盤側に立つ関数として予め記述しておくことができます。 このような関数をコールバック関数と呼びます。
コールバック関数を使うことにより具体的な実装を後回しにして処理を抽象化できることになります。 またこの抽象化により様々な場面で再利用可能な基盤とすることができるわけです。 このことはまたシステム全体の機能の拡張をも容易にします。 例えば今回の例で、後でAnimalの具体例を増やしたり減らしたりしたとしても、基盤側のactionAll関数を修正する必要はないわけです (基盤側の関数に不具合がなければですが)。
今回の例の実行結果を以下に示します。
==== Dog says ====
Bow wow!
==== Cat says ====
Meow meow!
==== Mouse says ====
Squeak squeak!
functype文のデフォルト引数と名前付き引数
functype文の仮引数にデフォルト引数を設定することもできます。
関数型変数を介した関数呼び出しにおいてfunctype文で宣言した関数型を明示的に用いた場合、 その関数型変数が指し示す実体側の関数のデフォルト引数ではなく、functypeで指定されたデフォルト引数が優先して設定されます。 例えば次の通りです。
functype FuncT( x int, y = 20 ) void
function func( x int, y = 10 ) void
{
Rrk_print( \=x \, \=y \n )
}
func( 0 ) /* A */
varia f = func
f( 1 ) /* B */
varia g FuncT = func
g( 2 ) /* C */
上記で呼び出しAでは、関数funcを直接呼び出していますので、この場合、当然ながら第2引数のデフォルト値は10となります。
次に呼び出しBでは、関数funcを変数fへ一旦代入したものを使っていますが、このfの型は型推論により関数funcと全く同じになります。 つまり関数funcを直接呼び出すのと同じですので、fの第2引数のデフォルト値はこちらも10となります。
最後に呼び出しCでは、関数funcを変数gへ一旦代入したものを使っていますが、このgの型は明示的にFuncTと宣言されています。 functype文によれば、FuncTの第2引数のデフォルト値は20となっていますので、gに設定されるデフォルト値は20となります。 (デフォルト引数の値が異なっていても引数型と戻り値の型が同じであれば、関数型同士で代入が可能(この例ではfuncをgへ代入することが可能)であることにも注意してください)。 実行結果は以下のようになります。
x=0, y=10
x=1, y=10
x=2, y=20
これにより、funcを使う側がそのデフォルト引数の値を(変数を介して呼び出す場合に限りますが)さらに上書きすることができます。
あるいはfunctype側でデフォルト引数を全く指定していなければ、変数を介した関数呼び出しでは
引数の省略を禁止することもできます。
また、関数型変数を介した関数呼び出しにおいてfunctype文で宣言した関数型を明示的に用いた場合、 名前付き引数での呼び出しではその関数型変数が指し示す実体側の仮引数の名前ではなく、functypeで設定された仮引数の名前を指定します。 例えば次の通りです。
functype FuncT( a int, b = 20 ) void
function func( x int, y = 10 ) void
{
Rrk_print( \=x \, \=y \n )
}
func( x:0 ) /* A */
varia f = func
f( x:1 ) /* B */
varia g FuncT = func
g( a:2 ) /* C */
上記で呼び出しAでは、関数funcを直接呼び出していますので、この場合、当然ながら名前付き引数は「x:値」や「y:値」という形になります。
次に呼び出しBは、関数funcを変数fへ一旦代入したものを使っていますが、このfの型は型推論により関数funcと全く同じになります。 つまり関数funcを直接呼び出すのと同じですので、名前付き引数はやはり「x:値」や「y:値」という形になります。
最後に呼び出しCは、関数funcを変数gへ一旦代入したものを使っていますが、このgの型は明示的にFuncTと宣言されています。 functype文によれば、FuncTの引数名は a, b となっていますので、 gへの名前付き引数は「a:値」や「b:値」という形になります。 実行結果は以下のようになります。
x=0, y=10
x=1, y=10
x=2, y=20
呼び出しCにおいては、指定した引数名が a, b ですが(外側からはそのように見えますが)、呼び出される実体は関数funcであることに変わりはありません。 そのため関数func内部においては、当然ながら引数名はそれぞれ x, y として扱われます。
functype文とコールバック関数のまとめ
関数型変数を宣言するにはfunctype文を用います。 関数型変数を使うと関数のエイリアスやコールバック関数などを使用することができます。 コールバック関数を使うことでプログラムの再利用性や拡張性を向上できます。
目次に戻る
無名関数
無名関数の記法
Rarakuでは関数そのものをリテラルとして記述することができ、 さらにこれを(functype文により定義した)関数型変数に直接代入することができます。 このようないわば関数型のリテラルを無名関数(anonymous function)と呼びます。
無名関数の書式は「^fun( 引数の指定 ) 戻り値」といった形になります。 これは通常の関数定義における書式で「function」キーワードの替わりに「^fun」演算子を使い、 さらに関数名の指定だけが無くなっている形になります。
例えば以下の例をご覧ください。
/* 関数型FuncT_BinOpの定義 */
functype FuncT_BinOp( refer x int, refer y int ) void
varia func_var FuncT_BinOp = ^fun( refer x int, refer y int ) void /* 無名関数 */
{
const tmp int = x
x = y
y = tmp
} /* ここでvaria文が終了 */
varia x = 10
varia y = 20
Rrk_print( "before : x=" x ", y=" y "\n" )
func_var( x, y ) /* 関数型の変数を介して関数(swap)を呼び出す。*/
Rrk_print( "after : x=" x ", y=" y "\n" )
この例は、最初にfunctype文によりFuncT_BinOpという名前の新しい関数型が定義されています。 次にvaria文により、FuncT_BinOp型の変数func_varを宣言していますが、この文の右辺に注目して下さい。 「=」の後ろに「^fun」演算子がきています。 また^funの後ろには関数名が指定されていないのでこれは無名関数です。 そしてここから右辺がとても長く5行ほど下がったところで「}」が現れますがここでこのvaria文が終了です。
このようにまず右辺で無名関数を定義し、それを変数func_varへ代入するといったようなことが同時にできます。
既に述べたとおり、関数型変数に代入できる関数は、その引数の型と戻り値の型がすべてマッチしているものだけになりますが、 この無名関数を代入する場合も同様に型がマッチしていなければなりません。 実際、今回の例ではFuncT_BinOpとマッチしています。
これを実行した結果を以下に示します。
before : x=10, y=20
after : x=20, y=10
尚、上の例ではvaria文の中で無名関数を使いましたが、const文や通常の代入文でも同様です。 例えば以下のようになります。
/* 関数型FuncTの定義 */
functype FuncT() void
const func_con FuncT = ^fun() void /* 無名関数 */
{
Rrk_print( "Call func_con\n" )
}
varia func_var FuncT
func_var = ^fun() void /* 無名関数 */
{
Rrk_print( "Call func_var\n" )
}
func_con()
func_var()
このように無名関数は一つのソースコードファイルの中にいくつも書くことができ、しかもそれぞれが別物とみなされます。
無名関数を使った場合、Rarakuが内部で自動的にユニークな関数名を割り振っていると考えても構いません。
実際、Rarakuの内部では各無名関数に「@_anonymous_imp整数_idx整数」といった形式の関数名を割り振って処理しています。
Rarakuユーザは「@」で始まる関数名を決して使うことはできないため、これらと名前が被ることはないわけです。
無名関数に関するコンパイルエラーが表示された場合、このような名前で表記されることもあります。
const/varia文で型の指定を省略し、かつ右辺に無名関数を指定して初期化した場合、 右辺に指定した無名関数に相当する型の変数/定数が宣言されたとみなされます。
例えば以下の例をご覧ください(func_varの部分です)。
varia func_var = ^fun( refer x int, refer y int ) void /* 無名関数 */
{
const tmp int = x
x = y
y = tmp
}
この例では func_var のvaria文に型は指定されていませんが、右辺に無名関数が指定されているため、 func_var は この無名関数に相当する型(refer int型引数を二つ持ち、戻り値がない関数型)で宣言されたとみなされます。
このときこの無名関数に相当する型をfunctype文で明示的に宣言していなくても構いません。 無名関数を使用するたびにfunctype文で予めそれに該当する型を宣言するのも面倒ですから、 ほとんどの場合このような型指定を省略した記法を使うことになると思います。
無名関数の場合、戻り値がないのであればvoidの記述の省略が可能です。
例えば上で挙げた無名関数では戻り値がありませんので以下のように書いてもOKです。
ちなみに、通常の関数においても戻り値がない場合にvoidを省略することができますが、 globalやnatvfuncキーワードがついた関数の場合はこのvoidを省略できません。
varia func_var = ^fun( refer x int, refer y int ) /* 無名関数の場合はvoidの指定を省略できる */
{
const tmp int = x
x = y
y = tmp
}
ちなみに、通常の関数においても戻り値がない場合にvoidを省略することができますが、 globalやnatvfuncキーワードがついた関数の場合はこのvoidを省略できません。
ループ文やswitch文のセクションで、continue文、break文、fallthrough文について説明しました。
また、これらをループ文やswitch文の外で使用することはできないことを述べました。
ここでさらに無名関数が絡む場合について若干補足の注意をしておきます。 以下の例をご覧ください。
上の例において、fallthrough 文は一見すると switch文(のcaseブロックまたはdefaultブロック)内にあるように見えますが、 よく見ると無名関数の中にあることがわかります。
この場合、外側のswitch文は無かったものとして無名関数内だけでチェックが行われます。 この無名関数内では switch文はなく、つまり fallthrough 文が直に記述されているとみなされコンパイルエラーとなります。
ここでさらに無名関数が絡む場合について若干補足の注意をしておきます。 以下の例をご覧ください。
varia kind = 0
switch kind {
case 0 :
const func = ^fun(){ /* 無名関数 */
fallthrough /* 無名関数内ではswitch文で囲まれていないため、コンパイルエラー */
}
default :
}
上の例において、fallthrough 文は一見すると switch文(のcaseブロックまたはdefaultブロック)内にあるように見えますが、 よく見ると無名関数の中にあることがわかります。
この場合、外側のswitch文は無かったものとして無名関数内だけでチェックが行われます。 この無名関数内では switch文はなく、つまり fallthrough 文が直に記述されているとみなされコンパイルエラーとなります。
無名関数を使ったコールバック関数の登録
コールバックの説明のところで取上げた例をもう一度考えましょう。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action
}
function actionAll( anim_ary Animal[] ) void
{
foreach anim : anim_ary {
Rrk_print( "==== " anim.name " says ====\n" )
Rrk_print( "\t" )
anim.bark()
}
}
function init( varia anim_ary Animal[], anim Animal ) void
{
Rrk_push_bk( anim_ary, anim )
}
/* 以下、具体的な実装 */
varia st_anim_ary Animal[]
function barkDog() void
{
Rrk_print( "Bow wow!\n" )
}
init( st_anim_ary, { "Dog", barkDog } )
function barkCat() void
{
Rrk_print( "Meow meow!\n" )
}
init( st_anim_ary, { "Cat", barkCat } )
function barkMouse() void
{
Rrk_print( "Squeak squeak!\n" )
}
init( st_anim_ary, { "Mouse", barkMouse } )
/* actionAllの実行 */
actionAll( st_anim_ary )
この例ではAnimalの具体的な実装(Dog, Cat, Mouse)において、barkDog、barkCat、barkMouse という関数をそれぞれ別名で記述しています。 これは勿論関数名が重複しては困るからですが、無名関数を使えばこれを気にせずに以下のように記述することができます。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action = ^fun(){}
}
function actionAll( anim_ary Animal[] ) void
{
foreach anim : anim_ary {
Rrk_print( "==== " anim.name " says ====\n" )
Rrk_print( "\t" )
anim.bark()
}
}
/* 以下、具体的な実装 */
varia st_anim_ary Animal[]
{
varia anim Animal = {}
anim.name = "Dog"
anim.bark = ^fun() /* 無名関数 */
{
Rrk_print( "Bow wow!\n" )
}
Rrk_push_bk( st_anim_ary, anim )
}
{
varia anim Animal = {}
anim.name = "Cat"
anim.bark = ^fun() /* 無名関数 */
{
Rrk_print( "Meow meow!\n" )
}
Rrk_push_bk( st_anim_ary, anim )
}
{
varia anim Animal = {}
anim.name = "Mouse"
anim.bark = ^fun() /* 無名関数 */
{
Rrk_print( "Squeak squeak!\n" )
}
Rrk_push_bk( st_anim_ary, anim )
}
actionAll( st_anim_ary )
この例ではbarkDog、barkCat、barkMouse という名前付き関数で記述する代わりに無名関数でそれらを書き、 同時に各ブロック内で宣言された一時的な構造体animのメンバ(この無名関数の型とマッチする関数型のメンバ)barkにそれぞれを代入しています。
上のように無名関数を使って書くと元のコードより行数も増え、一見複雑化しているように見えます。 しかしこのように無名関数を使った方が実際にはコードは元より改善されています。
例えば新しいAnimalのFoxを実装する場合を考えてみてください。 Dogの部分のコードをコピペしてそれをテンプレートとして名前と鳴き声の部分だけをFoxのものに修正すればよさそうですが、 元のコードでは、それ以外にも barkDog という関数名も barkFox などに忘れずに修正しなければならなくなります。 無名関数を使った実装ではその手間がなくなり、一段階管理しやすくなったコードになっていると言えます。
無名関数を初期化子の内部に記述する例
無名関数は初期化子の内部に書くこともできます。 例えば以下をご覧ください。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action
}
/* 初期化子を使って構造体animのnameとbarkを初期化 */
varia anim Animal = {
"Dog",
^fun() /* 無名関数 */
{
Rrk_print( "Bow wow!\n" )
}
}
上の例では初期化子を使って構造体animのメンバnameには"Dog"を、 同構造体のメンバbarkには無名関数をそれぞれ与え、構造体animを初期化しています。 構造体内のメンバの数が少ないような場合であれば、演算子「.」を使うよりこちらの方がすっきりと書けてよいかもしれません。
無名関数を関数の引数として記述する例
無名関数は関数の引数として記述することもできます。 例えば以下をご覧ください。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action = ^fun(){}
}
function set_bark( varia anim Animal, bark FuncT_Action ) void
{
anim.bark = bark
}
varia anim Animal = {}
anim.name = "Dog"
set_bark(
anim,
^fun(){ /* 関数set_barkの第2引数として無名関数を指定 */
Rrk_print( "Bow wow!\n" )
}
)
上の例の関数set_barkの第1引数animは構造体であり、関数内部でそのメンバbarkの値を更新しているため、variaモディファイアが必要となります。 第2引数はFuncT_Action型であり、set_barkの呼び出しでは第2引数に同じ型の無名関数が指定されています。 このように指定する無名関数の型は第2引数のFuncT_Action型とマッチする必要があります。
また無名関数を構造体の初期化子でラップし、さらにそれを関数の引数に指定するといったようなこともできます。 これを使えば最初のコールバックの例はさらに次のように書くこともできます。
functype FuncT_Action() void
struct Animal {
name conststr
bark FuncT_Action
}
function actionAll( anim_ary Animal[] ) void
{
foreach anim : anim_ary {
Rrk_print( "==== " anim.name " says ====\n" )
Rrk_print( "\t" )
anim.bark()
}
}
function init( varia anim_ary Animal[], anim Animal ) void
{
Rrk_push_bk( anim_ary, anim )
}
/* 以下、具体的な実装 */
varia st_anim_ary Animal[]
init( st_anim_ary,
{ /* 構造体の初期化子 */
"Dog",
^fun(){ /* 無名関数 */
Rrk_print( "Bow wow!\n" )
}
}
)
init( st_anim_ary,
{ /* 構造体の初期化子 */
"Cat",
^fun(){ /* 無名関数 */
Rrk_print( "Meow meow!\n" )
}
}
)
init( st_anim_ary,
{ /* 構造体の初期化子 */
"Mouse",
^fun(){ /* 無名関数 */
Rrk_print( "Squeak squeak!\n" )
}
}
)
/* actionAllの実行 */
actionAll( st_anim_ary )
上の例では一時的な構造体animを用意する必要もなくなります。
無名関数の内部からその外側にある変数にアクセスする場合
普通の関数と同様に、無名関数内部からもグローバルスコープにある変数にアクセスすることができます。 例えば以下をご覧ください。
varia global_var = 9 /* グローバルスコープで宣言された変数 */
varia func_var = ^fun()
{
/* OK: 無名関数の内部からglobal_varへアクセス */
Rrk_print( \=global_var\n )
}
func_var()
上の例では無名関数の内部からglobal_varへアクセスし、その値を表示しています。 実行結果は以下のようになります。
global_var=9
次にグローバルスコープにブロックがあり(以降これをグローバルブロックと呼びます)、 さらにその中に無名関数があるような場合を考えます。 例えば次のような状況です。
functype FuncT_Void( void ) void
varia func_var FuncT_Void^?
varia global_var = 9 /* グローバルスコープで宣言された変数 */
/* グローバルスコープにあるブロック(グローバルブロック) */
{
func_var = ^fun()
{
/* OK: 無名関数の内部からglobal_varへアクセス */
Rrk_print( \=global_var\n )
}
}
if func_var {
func_var()
}
上の例は特に問題ありません。 先ほどの例との違いは、func_varを無名関数で直接初期化せず、グローバルブロック内でそれを代入していることです。 グローバルブロックから出た後にfunc_varを呼び出しています。
次の話に入る前に、ここでグローバルスコープで宣言された変数とグローバルブロック内で宣言された変数の違いについて説明します。 グローバルスコープで宣言された変数とは、本当に一番外側の(つまりいかなるブロック内にもない)階層において宣言された変数です。 一方、グローバルブロック内で宣言された変数とは、関数の外ではありますがグローバルブロック内で宣言された変数であり、 ブロックが閉じるとその寿命を終えます。 例えば以下のようになります。
varia global_var = 9 /* グローバルスコープで宣言された変数 */
/* グローバルスコープにあるブロック(グローバルブロック) */
{
varia var_in_global_block = 8 /* グローバルブロック内で宣言された変数 */
}
ここから本題ですが、 Rarakuでは無名関数内からその外側の変数へアクセスする場合、 その変数はグローバルスコープで宣言された変数でなければなりません。 例えば次のようにグローバルブロック内で宣言された変数にアクセスするとコンパイルエラーになります。
functype FuncT_Void( void ) void
varia func_var FuncT_Void^?
varia global_var = 9 /* グローバルスコープのトップで宣言された変数 */
/* グローバルスコープにあるブロック(グローバルブロック) */
{
varia var_in_global_block = 9 /* グローバルブロック内で宣言された変数 */
func_var = ^fun() /* 無名関数 */
{
/* コンパイルエラー : 無名関数の内部からvar_in_global_blockへアクセスすることは許可されない. */
Rrk_print( \=var_in_global_block\n )
}
}
if func_var {
func_var()
}
上の例では、無名関数の内部からグローバルブロック内の変数 var_in_global_block へアクセスしようとしています。 Rarakuの無名関数ではこれは許可されません。 これはグローバルブロックを抜けた後で func_var が呼び出された場合、 var_in_global_block の寿命は(ブロックが終っているので)もう終了しているためです。
たまたま同じグローバルブロック内で func_var が呼び出された場合、
まだ var_in_global_block の寿命は終了しておりませんし、このようなアクセスが便利な場合もあります。
しかし一般には func_var がどのようなタイミングで呼び出されるかは予測困難であり、
そのためRarakuの無名関数ではこのようなアクセスを禁じています
ただし、上記のvar_in_global_blockが static 宣言されている場合は話は別で、その場合はアクセスが可能になります。 その場合のvar_in_global_block の寿命はブロックが終っても持続するためです。 例えば以下の通りです。
functype FuncT_Void( void ) void
varia func_var FuncT_Void^?
varia global_var = 9 /* グローバルスコープのトップで宣言された変数 */
/* グローバルスコープにあるブロック(グローバルブロック) */
{
static varia var_in_global_block = 9 /* グローバルブロック内で宣言されたstaticな変数 */
func_var = ^fun() /* 無名関数 */
{
/* OK : var_in_global_blockがstatic宣言された変数であればこれにアクセスすることは可能. */
Rrk_print( \=var_in_global_block\n )
}
}
if func_var {
func_var()
}
グローバルスコープにあるブロック内で宣言された変数へこのようなアクセスをしたい場合は、 常にそれをstatic宣言しておけばよいと考えて差し支えありません。 static宣言していない場合との差は単純にその寿命の存続期間だけです。
では関数内にある無名関数から(その外側の)関数内の変数へアクセスしようとした場合はどうなるでしょうか? つまり以下のような状況です。
functype FuncT_Void() void
function sample() FuncT_Void /* 通常の関数 */
{
varia var = 8
varia f = ^fun() /* 無名関数 */
{
/* コンパイルエラー : 無名関数の内部から(その外側の)関数内の変数 var へアクセスすることは許可されない. */
Rrk_print( "var=" var "\n" )
}
return f
}
varia func_var FuncT_Void = sample()
func_var()
Rarakuではこのようなアクセスも許可されません。 上の例では無名関数はまず変数 f に代入され、変数 f は return 文により戻り値となってグローバルスコープにある func_var へ代入され、 最後の行の func_var() でそれが呼び出されます。 しかしその時点で sample 内の変数 var は既に寿命を終えています。 そのため、これへのアクセスも禁止されるわけです。
ならば上記の var を先ほどと同様に static 宣言すればどうでしょうか? 確かにその場合であればアクセスが可能になります。その場合、var の寿命は関数が終っても持続するためです。 例えば以下の通りです。
functype FuncT_Void() void
function sample() FuncT_Void /* 通常の関数 */
{
static varia var = 8
varia f = ^fun() /* 無名関数 */
{
/* OK : 外側の関数内のstatic変数 var へのアクセスであれば可能 */
Rrk_print( "var=" var "\n" )
}
return f
}
varia func_var FuncT_Void = sample()
func_var()
しかしながら、これはあなたが本当に望む結果とは異なるかもしれません。 通常の関数内のローカル変数であれば、関数の呼び出し毎にその実体がスタック上に確保されますが、 一方static宣言されているローカル変数は(たとえその関数を複数回呼び出したとしても)その実体は終始一つとなります。 勿論それで十分な場合もありますが、もしあなたが望んだのが関数の呼び出し毎に別個に確保される変数へのアクセスであるなら、 これは何の解決策にもなっていないことになります。
プログラミング言語によってはこのようなローカル変数(static変数ではなく)へのアクセスを認めているものもあります。
そのようなものは一般的にはクロージャと呼ばれることが多いと思いますが、
そのカラクリはざっくり言えばそのような変数の寿命を内部的に引き伸ばしているというものです
(これを束縛変数から自由変数へ変換するとも言います)。
Rarakuではlambdaキーワードを用いてlambda式を定義することができ(これについては「lambda式」のセクションで詳述しますが)、 これを使えば(無名関数では許可されなかった)外側の関数のローカル変数へのアクセスが可能になります (つまりRarakuでのlambda式は(レキシカルスコープの意味での)クロージャとなっています)。 Rarakuでのlambda式は、要するにこのようなアクセスも可能である無名関数のようなものだと考えて頂いてさほど問題はないと思います。
しかしそれならば最初からlambda式の解説だけでよいのではと思われるかもしれません。 確かにそれはある意味正しいのですが、ここで無名関数の限界を知っておくことはlambda式の意義を理解することにも繋がりますので、 このチュートリアルではまず先に無名関数の解説を行っています。
また、lambda式さえあれば無名関数が本当に不要かといえばそうとも言い切れない部分もあります。 lambda式の場合、無名関数と比べて内部での変数管理の負荷がそれだけ増し、パフォーマンスが犠牲になります。 さらにそのような引き伸ばされた変数の寿命や値の遷移をプログラマが直感的に把握し辛いという問題は依然として残ります。 このとき例えばlambda式の外側にある関数のスコープが大きくなってくると、結局そのような変数はグローバル変数としての性質に 徐々に近づいていき、つまりグローバル変数と同様の問題を抱えることになります。 その他、そのlambda式が思いの外汎用的であることが後で分かり、それを一つの関数として独立させたい場合に 変数の依存性の除去に苦労するシナリオも想定できます。
その観点からすると実際のプログラミングでは、無名関数が選択肢に挙がる場合もそれなりにあると考えられます。 そのためRarakuでは無名関数とlambda式を両方を用意し、かつこれらを明確に区別しています。
Rarakuではlambdaキーワードを用いてlambda式を定義することができ(これについては「lambda式」のセクションで詳述しますが)、 これを使えば(無名関数では許可されなかった)外側の関数のローカル変数へのアクセスが可能になります (つまりRarakuでのlambda式は(レキシカルスコープの意味での)クロージャとなっています)。 Rarakuでのlambda式は、要するにこのようなアクセスも可能である無名関数のようなものだと考えて頂いてさほど問題はないと思います。
しかしそれならば最初からlambda式の解説だけでよいのではと思われるかもしれません。 確かにそれはある意味正しいのですが、ここで無名関数の限界を知っておくことはlambda式の意義を理解することにも繋がりますので、 このチュートリアルではまず先に無名関数の解説を行っています。
また、lambda式さえあれば無名関数が本当に不要かといえばそうとも言い切れない部分もあります。 lambda式の場合、無名関数と比べて内部での変数管理の負荷がそれだけ増し、パフォーマンスが犠牲になります。 さらにそのような引き伸ばされた変数の寿命や値の遷移をプログラマが直感的に把握し辛いという問題は依然として残ります。 このとき例えばlambda式の外側にある関数のスコープが大きくなってくると、結局そのような変数はグローバル変数としての性質に 徐々に近づいていき、つまりグローバル変数と同様の問題を抱えることになります。 その他、そのlambda式が思いの外汎用的であることが後で分かり、それを一つの関数として独立させたい場合に 変数の依存性の除去に苦労するシナリオも想定できます。
このようにlambda式の場合、無名関数と比べてその外部の要素と密接に結びつくことになります。
このような状況を「結合が密(tight coupling)」と呼びます。
一般に「結合が密」の状態は上述の通りあまり望ましいものではありません。
その観点からすると実際のプログラミングでは、無名関数が選択肢に挙がる場合もそれなりにあると考えられます。 そのためRarakuでは無名関数とlambda式を両方を用意し、かつこれらを明確に区別しています。
無名関数内にある無名関数から(その外側の)無名関数内の変数へアクセスしようとした場合も同様です。 つまり以下のような状況です。
functype FuncT_Void() void
varia func_var2 = ^fun() FuncT_Void /* 無名関数 */
{
varia var = 8
varia func_var = ^fun() /* 無名関数 */
{
/* コンパイルエラー : 無名関数の内部から(その外側の)無名関数内の変数 var へアクセスすることは許可されない. */
Rrk_print( "var=" var "\n" )
}
return func_var
}
varia st_func_var FuncT_Void = func_var2()
st_func_var()
複雑な例ですが、最初の無名関数がその内部で定義した無名関数を戻り値として返しています。 それを最後の2行で受け取り、st_func_var() を呼び出すことで最終的に内部で定義した無名関数を呼び出しています。 この場合も同様に最初の無名関数内の var の寿命は終っており、var へのアクセスは禁止されます。
この例でも、もし var が static ローカル変数であればアクセス可能ですが、
そうなると今度は終始一つの変数となってしまいます。
staticでないローカル変数にアクセスしたい場合はやはり無名関数ではなく(後のセクションで述べる)lambda式を使う必要があります。
無名関数の再帰呼び出しとローカル関数
無名関数は通常の関数と同様に再帰呼び出し(recursive call)することができます。
ただここで注意しなければならないのは、再帰呼び出しの場合はvaria/const文の右辺に直接無名関数を指定して初期化する方法では うまくいかないということです。 例えば以下はコンパイルエラーとなります。
const factorial = ^fun( x int ) int { /* コンパイルエラー: 右辺の評価の段階ではfactorialはまだ宣言されていない */
if x > 2 {
..x * factorial( x-1 )
}
..x
}
Rrk_print( factorial( 4 )\n )
上記で「..」はシンタックスシュガーの一種でキーワードreturnと全く同じです。 一見これでうまくいきそうに見えるかもしれませんが、右辺の無名関数を評価する段階ではまだこのconst文は完了していないため、 factorialの宣言が終っていません。それにもかかわらず無名関数定義の内部でこのfactorialにアクセスしようとしているため、 Rarakuは次のようなコンパイルエラーを出します。
Cannot resolve or accesse the identifier [factorial].
もっと単純化していえば、これは「const x int = x」のように、宣言時の右辺として自分自身を記述するとコンパイルエラーになるのと同じようなものです。
よってここでは若干工夫が必要になります。 まずvaria文で初期化なしで変数factorialを宣言し、次の文で改めてfactorialに無名関数を代入します。 例えば以下のようになります。
functype FuncT( x int ) int
function dummy( x int ) int { return 0 }
{
static varia factorial FuncT = dummy /* 一旦宣言を終らせる */
factorial = ^fun( x int ) int {
if x > 2 {
..x * factorial( x-1 ) /* OK: factorial は確実に宣言されている */
}
..x
}
Rrk_print( factorial( 4 )\n )
}
上の例では4の階乗を計算していますので、結果は24と表示されます。
factorialの宣言は「varia」ではなく「static varia」とします。 factorial がたまたまグローバルスコープにある場合ならばどちらでもいいのですが、 これがなんらかのブロックの内にある場合(上記の例ではグローバルブロック内にありますが)、「static varia」が必要となります (単に「varia」とすると 無名関数 ^fun 内から外部の factorial を参照することができずコンパイルエラーとなります)。
これで一応目的は達成できているのですが、この方法ではfactorialを定数ではなく一旦変数として宣言しなければなりません。 つまりfactorialに再代入できてしまいます。 これが気持ち悪いならば final 文を使えばそのような再代入を封じることができます。
functype FuncT( x int ) int
function dummy( x int ) int { return 0 }
{
static varia factorial FuncT = dummy /* 一旦宣言を終らせる */
factorial = ^fun( x int ) int {
if x > 2 {
..x * factorial( x-1 ) /* OK: factorial は確実に宣言されている */
}
..x
}
final factorial /* factorialへの再代入はこれ以降禁止 */
Rrk_print( factorial( 4 )\n )
}
これでおよそ万全ではあるのですが、とは言え再帰呼び出しが必要になる度に毎回このように書くのも面倒です。 実は以下のようにキーワード function を使って関数を定義しますと、 上記の処理(関数型変数のstatic宣言、無名関数の代入、final文の3つの文)を、一気に行うことができます。
{
function factorial( x int ) int {
if x > 2 {
..x * factorial( x-1 )
}
..x
}
Rrk_print( factorial( 4 )\n )
}
これにより functype文 で FuncT 型を予め宣言しておく必要もなくなり、かなりスッキリします。
ところで、これはよく見ると通常の関数定義と全く同じ書式です。 今までと何が違うのでしょうか? 実は今までの関数定義はすべて(最上層の)グローバルスコープにおいて行っていました。 この場合、同じファイル内からならばどこからでもその関数を呼び出すことができます。 例えばその関数定義よりも順序が前に位置するコードからそれを呼び出すことも可能です。
一方、上記は無名ブロック(グローバルブロック)内の内部で関数定義を記述しています。 このような関数を定義した場合、その関数定義よりも順序が前に位置するコードからそれを呼び出すことはできません。 またこの無名ブロックの外側から、その関数を呼び出すこともできません。 このようなブロック内の内部での関数定義をローカル関数と呼びます。
キーワードfunctionによる関数定義は、グローバルスコープにおける場合と、 今回のように無名ブロック内の内部で行った場合とで意味が変わることに注意しましょう。 前者は通常の関数の定義となり、後者がローカル関数の定義となります。
このローカル関数の定義で内部的に行われているのはまさに上述した3つの文に相当する処理であり、
その実体はfinalizeされた関数型変数になります。
そのためこれを呼び出せるのはその宣言より下からだけとなるわけです。
また無名ブロック内に宣言された変数ですので、そのスコープはその無名ブロックが終ると無効になります。 つまりこの無名ブロックより外側から呼び出すこともできないわけです。 ただしローカル関数内でリカーシブコールすることは可能になっています。
また無名ブロック内に宣言された変数ですので、そのスコープはその無名ブロックが終ると無効になります。 つまりこの無名ブロックより外側から呼び出すこともできないわけです。 ただしローカル関数内でリカーシブコールすることは可能になっています。
ローカル関数の定義は式ではなく(通常の関数定義と同じく)文です。
つまり無名関数のようにその定義自体をリテラルとして他の式中に埋め込むといったようなことはできません。
通常の関数と比べて制限が強くなるなら、ローカル関数に何の魅力もないと思われるかもしれません。 しかし実際のプログラミングにおいては、可能ならば極力ローカル関数にすべきです。
小規模なプログラミングでは管理についてそれほどシビアに考える必要はありませんが、 一方でその規模が大きくなると全体をいかに管理するかが重要な課題となってきます。 全体を意味ある範囲で分割し、分割した個々において一度に考慮すべき範囲を狭くすることで、 全体の管理がしやすくなります。
例えば簡単のためまず変数の管理について考えてみます。
局所的にしか使用しない変数は、それが有効な範囲(つまりスコープ)を極力狭くすべきです。 そうすれば「その値が変わる可能性のある範囲」もまた狭くでき、その分思考の負荷が減少します。 これは結果的にバグを発生させる確率を下げます。あるいはいざバグが発生した場合、その原因の解析を容易にします。
逆にそれが有効な範囲を無駄に広げることで、「その値が変わる可能性のある範囲」も広くなり、 その分思考の負荷が増大します。 これは結果的にバグを発生させる確率を上げます。あるいはいざバグが発生した場合、その原因の解析を困難にします。
関数の管理についても変数の場合と同様です。 そのブロック内でしか使用しない関数であれば、ローカル関数にすることによりブロック外部との関係性を遮断でき、 その分思考の負荷を減少させることができます。
局所的にしか使用しない変数は、それが有効な範囲(つまりスコープ)を極力狭くすべきです。 そうすれば「その値が変わる可能性のある範囲」もまた狭くでき、その分思考の負荷が減少します。 これは結果的にバグを発生させる確率を下げます。あるいはいざバグが発生した場合、その原因の解析を容易にします。
逆にそれが有効な範囲を無駄に広げることで、「その値が変わる可能性のある範囲」も広くなり、 その分思考の負荷が増大します。 これは結果的にバグを発生させる確率を上げます。あるいはいざバグが発生した場合、その原因の解析を困難にします。
関数の管理についても変数の場合と同様です。 そのブロック内でしか使用しない関数であれば、ローカル関数にすることによりブロック外部との関係性を遮断でき、 その分思考の負荷を減少させることができます。
型隠蔽演算子による戻り値の型推論
まず以下の例をご覧ください。
const calcPolynomial = ^fun( x int ) int
{
return 3 * x*x + 2 * x + 5
}
Rrk_print( calcPolynomial( 10 ) \n )
上記では、xがint型ですので「3 * x*x + 2 * x + 5」という式もまたint型であり、 関数の戻り値としてint型を返すことがreturn文に与えられた式からわかります。 このような場合、関数の戻り値の型の部分を「->」と書くことでその部分を自動的に型推論させることができます。 これをRarakuでは型隠蔽演算子と呼びます。
キャスト演算子と同じく「->」を使用しますが、両者は別のものです。
キャスト演算子は式中に現れ、後ろに型名が続きます。 一方、型隠蔽演算子は関数定義や構造体定義において現れ、後ろに「{」が続きます。
構造体定義においてこれを用いると不完全型構造体と呼ばれるものを定義できますが、 これについては「モジュールと情報隠蔽」のセクションで詳しく述べます。
キャスト演算子は式中に現れ、後ろに型名が続きます。 一方、型隠蔽演算子は関数定義や構造体定義において現れ、後ろに「{」が続きます。
構造体定義においてこれを用いると不完全型構造体と呼ばれるものを定義できますが、 これについては「モジュールと情報隠蔽」のセクションで詳しく述べます。
例えば上記は以下のように記述することができます。
const calcPolynomial = ^fun( x int )->
{
return 3 * x*x + 2 * x + 5
}
Rrk_print( calcPolynomial( 10 ) \n )
ただしこのように書くと後でこれを人間が読む際に、その関数の戻り値の型が何であるかが中身のreturn文を確認しないとわからなくなります。
それならば、もうこのような演算子を使わず素直に「int」と書けばよいと思われるかもしれません。
確かに今回のように「int」型の替わりとしての使用であればその通りなのですが、
これが関数型になると「->」を使った方がずっと簡潔になる場合があります。
これについては「lambda式」のセクションで改めて考察しましょう。
一方「->」が使用できないケースもあります。 Rarakuでは最初のreturn文によって戻り値の型が明確になる前に再帰呼び出しが存在する場合、「->」による型推論ができないという仕様になっています。 例えば以下のように再帰呼び出しを伴う場合、「->」を使用することはできません。
{
function factorial( x int )->{
if x > 2 {
..x * factorial( x-1 ) /* コンパイルエラー: この部分の型が推論できない */
}
..x
}
Rrk_print( factorial( 4 )\n )
}
上記ではコンパイルエラーとなり、以下のように表示されます。
The return type of the calling function cannot be inferred.
Please specify it explicitly.
「->」を指定した場合、Rarakuコンパイラは関数内を辿って一番最初に現れるreturn文を解析し、 そのreturn文で指定されている式の型より関数の戻り値の型を推論します。 上記の場合、最初のreturn文(上記では「..」となっていますが)は「..x * factorial( x-1)」ですが、 この中に再びfactorialが含まれています。 そしてまだfactorialの戻り値は不定であるため、ここで解析が中断されエラーとなるわけです。
もっとも上記の場合、その下にあるreturn文(つまり「..x」ですが)まで辿ればint型とわかるのですが、
現在のRarakuコンパイラはそこまでは解析しません。
言語仕様の背景
言語仕様の背景
Rarakuでは基本的に関数呼び出しが現れた時点で、呼び出される関数の定義部の戻り値の型を先に解析し確定させます。
ただしそれが再帰呼び出しの場合、自分自身の定義部の解析がまだ完全には終了していませんので、
関数定義部の代わりに関数宣言の情報より戻り値の型を判断します。
一方、このような関数呼び出しの解析を一旦保留して後回しにする方法も考えられますが、
Rarakuではそのような方法は採用していません。
一つ目の理由は、コードの下の方にある return 文の解析を終えた後に改めてコードの上へ戻り、 保留しておいた関数呼び出しの解析を行うような方法はやはりコンパイラの実装が複雑となりますし、 その解析を後回しにした影響が他の部分の解析に波及する可能性もあるためです。
二つ目の理由は、現在のRarakuのような仕様にしておけば、次のような型推定が原理的に不可能なコードを自然にコンパイルエラーにできるためです。
上記の例で、(無限に再帰呼び出ししているという別の問題もありますが、今はそこを論点とせず) Rarakuコンパイラが関数 func の定義部を解析することを考えます。 「varia a = func()」において、func() という関数呼び出しの式が出現していますが、 この時点では、Rarakuコンパイラはまだ自分自身の関数funcの戻り値の型を確定できていません。
今、仮にこの戻り値の型の解析を一旦保留したとして、次の文 return a の解析を先に行うとします。 aの型は「varia a = func()」文において、funcの戻り値の型を元に型推論されますが、まだfuncの戻り値の型が確定できていないため、 aの型推論も不可能となります。 そのため、結局 return a の解析を先に行うことも不可能です。
ちなみに上記の例の「varia a = func()」の部分を、例えば「varia a int = func()」などと書けば、 「return a」での型推論は原理的には可能になります。 しかし上で述べた一つ目の理由により、やはりRarakuコンパイラではこのような方法を採用していません。
Close
一つ目の理由は、コードの下の方にある return 文の解析を終えた後に改めてコードの上へ戻り、 保留しておいた関数呼び出しの解析を行うような方法はやはりコンパイラの実装が複雑となりますし、 その解析を後回しにした影響が他の部分の解析に波及する可能性もあるためです。
二つ目の理由は、現在のRarakuのような仕様にしておけば、次のような型推定が原理的に不可能なコードを自然にコンパイルエラーにできるためです。
/* コンパイルエラー:funcの戻り値や変数aの型を型推定することが原理的に不可能 */
function func()->{
varia a = func()
return a
}
上記の例で、(無限に再帰呼び出ししているという別の問題もありますが、今はそこを論点とせず) Rarakuコンパイラが関数 func の定義部を解析することを考えます。 「varia a = func()」において、func() という関数呼び出しの式が出現していますが、 この時点では、Rarakuコンパイラはまだ自分自身の関数funcの戻り値の型を確定できていません。
今、仮にこの戻り値の型の解析を一旦保留したとして、次の文 return a の解析を先に行うとします。 aの型は「varia a = func()」文において、funcの戻り値の型を元に型推論されますが、まだfuncの戻り値の型が確定できていないため、 aの型推論も不可能となります。 そのため、結局 return a の解析を先に行うことも不可能です。
ちなみに上記の例の「varia a = func()」の部分を、例えば「varia a int = func()」などと書けば、 「return a」での型推論は原理的には可能になります。 しかし上で述べた一つ目の理由により、やはりRarakuコンパイラではこのような方法を採用していません。
Close
ただこの例の場合であれば、少し書き方を工夫することで問題は解消します (そもそも「->」を使わずに素直にintと書けばそれで解決なのですが、ここでは敢えて「->」を使うとしましょう)。 要するに再帰呼び出しが現れるより前に、一番最初に現れるreturn文で戻り値型が明確にされさえすればよいのですから、 例えば以下のように「..x」の方を先に持ってくればよいわけです(実行結果は元のものと変わりません)。
{
function factorial( x int )->{
if x <= 2 { /* 「..x」を先に持ってきたため、条件を逆にする */
..x
}
..x * factorial( x-1 )
}
Rrk_print( factorial( 4 )\n )
}
無名関数の即時呼び出し
無名関数の即時呼び出しについて参考までに紹介します。 これは無名関数の定義と同時に、それを即時に呼び出すというものです。
以下のようにまず無名関数の定義全体を式として書きます。 その後に引数部(以下の例では最後の「( x )」の部分)を続ける形になります。
varia x = 10
/* 無名関数の即時呼び出し */
^fun( x int ){
varia y int = x * x + 2 * x + 1
Rrk_print( "Immediate call. y=" y "\n" )
}( x )
ご覧のとおり可読性はあまり良くはありません。 一番後ろを見るまでこれが即時呼び出しであるかどうかの判断すらつきません。
このような形式を初めて見る方は、まず X( x ) という関数呼び出しを先にイメージすると分かりやすいかもしれません。 ここでは X にあたる式の部分が無名関数の定義全体となっています。 つまり無名関数の定義全体があくまで一つの(関数型の)式として扱われます。
ちなみにJavascriptでこのような呼び出しを行う場合、無名関数の定義全体をさらに括弧で囲う必要がありますが、
Rarakuではそのような括弧で囲っても囲わなくてもどちらでも構いません。
通常の関数やローカル関数は式ではなく定義文であるため、このような即時呼び出しをすることはできません。
参考 : 無名関数の即時呼び出しをどう使うか?
一応紹介はしましたが、Rarakuではこのような即時呼び出しの使いどころはあまりないと思います。
実際、上の例のような戻り値のないケースでは、そもそも(無名)ブロックで次のように書けば十分です。
一方、戻り値のあるようなケースでは(無名)ブロックでは対応できません(今、通常の関数は使わないとしましょう)。 この場合、例えば以下のようにconst文の初期化において無名関数の即時呼び出しを使うこともできます (比較のため三項演算子で同様の処理を書いた場合も下に記述しています)。
上記で「..」はシンタックスシュガーの一種で「return」と全く同じです。
三項演算子は本質的にはif-elif-else処理ですが、無名関数ではswitch文を使うことができます。 switch文の方が変数がコード内に現れる回数は減り一般的にはその方が良いとも言えますが、 しかし実際のところどちらが見やすいかと言われるとこの例ではあまり差はないようにも見えます。
ではさらに内部にループ文を使わざるを得ないような(const文での)初期化であればどうでしょうか? この場合そもそも三項演算子では対応できません。 例えば以下のような場合です(比較のため「無名関数を一旦変数convertに代入してそれを呼び出した場合」も下に記述しています)。
この例では列挙型のveg_aryをすべて文字列に変換した配列veg_name_aryを、二通りの方法で初期化しています。 一つ目が無名関数の即時呼び出しにより一気に記述する方法、もう一つが無名関数を一旦変数convertに代入してそれを呼び出す方法です。 前者では変数convertを使わずに済んではいますが、結局どちらも大差ないように見えます。 むしろ後者のように分けて書く方が構文的な複雑さが一段階緩和され、さらにconvertという変数名が自然にコメントとしての役割も果たすため、 見やすいかもしれません。
もっと複雑な例を考えることもできますが、その場合でも結局のところ無名関数の即時呼び出しを積極的に使う理由はあまり見当たらないと思います。
ただ、後述するlambda式を内部で使用することまで許すならば、無名関数の即時呼び出しをつかって Javascript等でよく行われるモジュールパターンと呼ばれる手法を実現できます。 これについては「モジュールと情報隠蔽」のセクションで詳しく説明します。
Close
varia x = 10
{
varia y int = x * x + 2 * x + 1
Rrk_print( "Immediate call. y=" y "\n" )
}
一方、戻り値のあるようなケースでは(無名)ブロックでは対応できません(今、通常の関数は使わないとしましょう)。 この場合、例えば以下のようにconst文の初期化において無名関数の即時呼び出しを使うこともできます (比較のため三項演算子で同様の処理を書いた場合も下に記述しています)。
enum Vegetable {
Tomato
Onion
Lettuce
Carrot
Potato
Broccoli
Burdock
Pumpkin
}
const veg Vegetable = Pumpkin
/* 無名関数の即時呼び出しを使って書いた場合 */
const price1 int = ^fun( v Vegetable ) int {
switch v {
case Tomato, Onion, : ..100
case Lettuce, Carrot, : ..200
case Potato, Broccoli, Pumpkin, : ..300
}
..400
}( veg )
/* 三項演算子を使って書いた場合 */
const price2 int =
veg == Tomato || veg == Onion ? 100 :
veg == Lettuce || veg == Carrot ? 200 :
veg == Potato || veg == Broccoli || veg == Pumpkin ? 300 :
400
上記で「..」はシンタックスシュガーの一種で「return」と全く同じです。
三項演算子は本質的にはif-elif-else処理ですが、無名関数ではswitch文を使うことができます。 switch文の方が変数がコード内に現れる回数は減り一般的にはその方が良いとも言えますが、 しかし実際のところどちらが見やすいかと言われるとこの例ではあまり差はないようにも見えます。
ではさらに内部にループ文を使わざるを得ないような(const文での)初期化であればどうでしょうか? この場合そもそも三項演算子では対応できません。 例えば以下のような場合です(比較のため「無名関数を一旦変数convertに代入してそれを呼び出した場合」も下に記述しています)。
enum Vegetable {
Tomato
Onion
Lettuce
Carrot
Potato
Broccoli
Burdock
Pumpkin
}
/* 列挙型の配列 */
const veg_ary Vegetable[] = [ Tomato, Onion, Pumpkin ]
/* 無名関数の即時呼び出しによって一気に記述した場合 */
{
/* 列挙型の名前の配列 */
const veg_name_ary = ^fun( v_ary Vegetable[] ) conststr[] {
varia s_ary conststr[]
foreach v : v_ary {
/* v を conststr型に変換し、それを conststrの配列s_aryへ追加 */
Rrk_push_bk( s_ary, v->conststr )
}
..s_ary
}( veg_ary )
}
/* 無名関数を一旦変数convertに代入してそれを呼び出した場合(即時呼び出しではない) */
{
const convert = ^fun( v_ary Vegetable[] ) conststr[] {
varia s_ary conststr[]
foreach v : v_ary {
/* v を conststr型に変換し、それを conststrの配列s_aryへ追加 */
Rrk_push_bk( s_ary, v->conststr )
}
..s_ary
}
/* 列挙型の名前の配列 */
const veg_name_ary = convert( veg_ary )
}
この例では列挙型のveg_aryをすべて文字列に変換した配列veg_name_aryを、二通りの方法で初期化しています。 一つ目が無名関数の即時呼び出しにより一気に記述する方法、もう一つが無名関数を一旦変数convertに代入してそれを呼び出す方法です。 前者では変数convertを使わずに済んではいますが、結局どちらも大差ないように見えます。 むしろ後者のように分けて書く方が構文的な複雑さが一段階緩和され、さらにconvertという変数名が自然にコメントとしての役割も果たすため、 見やすいかもしれません。
もっと複雑な例を考えることもできますが、その場合でも結局のところ無名関数の即時呼び出しを積極的に使う理由はあまり見当たらないと思います。
Javascriptではいわゆる「グローバル汚染」と呼ばれる問題が(古いバージョンには)あり、このような記法がよく使われていたという歴史的背景があります。
一方Rarakuではそもそもそのような問題が最初から存在しないため、このような無名関数の即時呼び出しの必要性は低いです。
とはいえ言語仕様で明確に禁じるほどのことでもないので一応そのような記法が可能にはなっています。
ただ、後述するlambda式を内部で使用することまで許すならば、無名関数の即時呼び出しをつかって Javascript等でよく行われるモジュールパターンと呼ばれる手法を実現できます。 これについては「モジュールと情報隠蔽」のセクションで詳しく説明します。
Close
無名関数のまとめ
無名関数を使うとコールバック関数の登録を整理して簡易に行うことができます。 あるいはconst/varia文で右辺に無名関数を指定して初期化しますと、関数型変数を介して無名関数を呼び出すこともできます。 (このときconst/varia文の型の指定は省略できるため、予めfunctype文でその型を宣言しておく必要はありません)。 関数の中に無名関数を書くこともできますが、ただしその場合、外側の関数のローカル変数にアクセスすることはできませんので注意しましょう。
ブロック内で(functionキーワードにより)関数定義することもでき、それをローカル関数と呼びます。 そのブロック内でしか使用しない関数であれば、ローカル関数にすることにより後々の管理がしやすくなります。 グローバルスコープで行う通常の関数定義との違いにも注意しましょう。
目次に戻る
lambda式
lambda式とは
lambda式(ラムダ式)について全く初めてであれば、まずざっくりこれは無名関数と同じようなものだと捉えた方がいいでしょう。 無名関数では「^fun」演算子を使いましたが、lambda式ではそれを単に「^lam」演算子に置き換えて使うことができます。 実際、lambda式は関数型変数にそのまま格納することもできます。 よって使い方自体は無名関数と(使用する演算子以外は)見かけ上変わりません。
lambda式ではないとできないような細かいテクニック等はまずは置いておきましょう。 今の段階で一番重要なのは、なぜ無名関数ではなくわざわざlambda式を使うのかという疑問をまずは解消することです。
「無名関数」のセクションで詳しく述べていますが、無名関数ではその外側の関数のローカル変数へアクセスは許可されません。 一方、lambda式ではそれが許可されます。 無名関数とlambda式の違いは、言ってしまえばそれだけです (「無名関数」のセクションを一読してからこのセクションを読むとより差が分かりやすいかと思います)。
クロージャをご存知の方は、次のように考えて支障ありません。
すなわち^fun演算子で定義された無名関数はクロージャではなく、
^lam演算子で定義された無名関数はクロージャになるということです。
一般的にはlambda式もまた無名関数の一部のように説明されることが多いと思いますが、
Rarakuでは一応前者と後者を区別し、前者を「無名関数」、後者を「lambda式」と呼んでいます。
記法上は^fun演算子の替わりに^lam演算子を指定し、それをあたかも無名関数と同様に取り扱うだけです。 ただしlambda式は無名関数と比べ、ローカル関数の管理のための色々な下準備を内部で行うため、若干のオーバーヘッドが掛かります。 とはいえ多くの場合、そのようなオーバーヘッドは無視できる範囲かと思います。 速度を重視し、しかも機能的にも無名関数で十分事足りるような場合であれば、lambda式よりも無名関数の方が良いかもしれません。
無名関数とrefer引数とlambda式
次に具体的な例を用いて説明します。
既に述べた通りRarakuでは無名関数内からその外側の変数へのアクセスは(グローバル変数やstatic変数を除き)許可されません。 しかしそうは言っても、ある関数内のあるローカル変数の寿命が存続する期間にとりあえず限定するとして、 以下のように何か下請け関数を作っておいてそれを関数内で使いまわしたいことはあります (ただし以下はRarakuではコンパイルエラーとなります)。
function my_func() void
{
const print = Rrk_print
varia a = 1
varia b = 2
varia c = 3
/* 下請け関数 : a, b, cを係数とした2次多項式を計算 */
const updatePolynomial = ^fun( x int ) int
{
/* コンパイルエラー: 外側の関数内のローカル変数a, b, cは参照できない */
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
}
/* 結果表示 */
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x) \n )
print( \n )
}
a = 2 b = -3 c = 4
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x) \n )
print( \n )
}
}
my_func()
このような場合、無名関数ですと a、b、cをupdatePolynomialの引数として渡すしかありません。 ただし今回の場合、それらの更新を伴いますから少し工夫が必要です。
例えば一旦構造体を定義してそのメンバとしてa、b、cを含ませておき、 それをupdatePolynomialの引数として与え、それぞれを更新する方法もありますが若干面倒です。 またその場合、その構造体をこの関数の外で定義しなければなりません。
もう一つの方法は、refer型の引数 a、b、cを updatePolynomialの引数として設けることです。 例えば次のようになります。
function my_func() void
{
const print = Rrk_print
varia a = 1, b = 2, c = 3
/* 下請け関数 : a, b, cを係数とした2次多項式を計算 */
const updatePolynomial = ^fun( x int, refer a int, refer b int, refer c int ) int
{
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
}
/* 結果表示 */
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x,a,b,c) \n )
print( \n )
}
a = 2 b = -3 c = 4
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x,a,b,c) \n )
print( \n )
}
}
my_func()
引数を逐一指定しなければならない面倒さはありますが、この程度であれば許容できる範囲かと思います。 また実行速度を重視するならば、Rarakuではこの無名関数と引数を用いた書き方が多分最速になります。 一応、この例の実行結果も以下に示しておきます。
a=1, b=2, c=3
When x=1 : ans=6
a=2, b=3, c=4
When x=2 : ans=18
a=3, b=4, c=5
When x=3 : ans=44
a=2, b=-3, c=4
When x=1 : ans=3
a=3, b=-2, c=5
When x=2 : ans=13
a=4, b=-1, c=6
When x=3 : ans=39
一方、さらに沢山の引数をとらなければならない複雑な状況なら、引数を構造体にすることを検討したり、 最速の実行速度を求めないならlambda式を使ってこの引数の指定自体を不要にできます。 これは一番初めのコードにおける(updatePolynomialでの)演算子^funを、以下のように演算子^lamに変えるだけです。
function my_func() void
{
const print = Rrk_print
varia a = 1, b = 2, c = 3
/* 下請け関数 : a, b, cを係数とした2次多項式を計算 */
const updatePolynomial = ^lam( x int ) int
{
/* OK: lambda式では外側の関数内のローカル変数a, b, cですらも参照可能 */
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
}
/* 結果表示 */
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x) \n )
print( \n )
}
a = 2 b = -3 c = 4
for x:=1; x<4; ++x {
print( \=a \, \=b \, \=c \n )
print( "When " \=x " : ans=" updatePolynomial(x) \n )
print( \n )
}
}
my_func()
上の例で updatePolynomial は今度はlambda式を指し示す変数となりますが、これを通常の関数型変数と同様に呼び出して使うことができます。 この例の実行結果は勿論、先ほどと全く同じになります。
無名関数のセクションで型隠蔽演算子「->」について説明しましたが、これはlambda式でも同様に使用することができます。
例えば上記のupdatePolynomialは以下のように記述することもできます。
ただし無名関数のセクションでも延べましたが、このように書くと今度はこれを人間が読む際、 そのlambda式の戻り値の型が何であるかが中身のreturn文を確認しないとわからなくなります。
const updatePolynomial = ^lam( x int )->
{
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
}
ただし無名関数のセクションでも延べましたが、このように書くと今度はこれを人間が読む際、 そのlambda式の戻り値の型が何であるかが中身のreturn文を確認しないとわからなくなります。
lambda式の型と関数型変数
lambda式は通常の関数や無名関数と同様に関数型変数に格納することができます。 例えば以下の通りです。
functype FuncT_II( x int ) int
function my_func( update FuncT_II, const x_ary int[] ) void
{
foreach x : x_ary {
Rrk_print( "When " \=x " : ans=" update(x) \n )
}
}
{
varia a = 1, b = 2, c = 3
my_func(
^lam( x int ) int {
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
},
[ 1, 2, 3 ]
)
}
上の例で関数my_funcの第1仮引数はFuncT_II型となっていますが、これは「int型引数を一つとりint型を返す」関数です。 一方、my_funcの第1引数に実際に与えているのは、「int型引数を一つとりint型を返す」lambda式です。 関数とlambda式の違いはありますが、「int型引数を一つとりint型を返す」挙動をとるのは共通しており、 この場合両者は同じ型とみなされ、このように代入ができます。
ちなみにこの例で^lamを^funにするとコンパイルエラーとなります。
(グローバルスコープのトップではない)グローバルブロック内の a, b, c への(無名関数内からの)参照となるためです。
この例の実行結果は以下のようになります。
When x=1 : ans=6
When x=2 : ans=18
When x=3 : ans=44
前の項の例では、以下のようなconst文を記述していました。
varia a = 1 b = 2 c = 3
/* 下請け関数 : a, b, cを係数とした2次多項式を計算 */
const updatePolynomial = ^lam( x int ) int
{
/* OK: lambda式では外側の関数内のローカル変数a, b, cですらも参照可能 */
const ans int = a * x*x + b * x + c
++a; ++b; ++c;
return ans
}
上のコードでupdatePolynomialには型が指定されていませんが、これは自動的に 「int型引数を一つとりint型を返す」関数型変数であると推論されます。 右辺にそのようなlambda式が指定されているからです。 無名関数ではこのような場合、const/varia文の型指定を省略できましたが、これはlambda式でも全く同様です。 このとき「int型引数を一つとりint型を返す」関数の型をfunctype文で予め宣言しておく必要はありません。
lambda式の再帰呼び出し
lambda式も無名関数と同様に再帰呼び出し(recursive call)することができます。
ただしlambda式でも無名関数と同様の注意事項があります。 つまりvaria/const文に直接lambda式を指定する方法ではうまくいきません。 例えば以下はコンパイルエラーとなります。
{
varia alternative = 0
const factorial = ^lam( x int ) int { /* コンパイルエラー: 右辺の評価の段階ではfactorialはまだ宣言されていない */
if x > 2 {
..x * factorial( x-1 )
}
if x <= 0 {
..alternative
}
..x
}
Rrk_print( factorial( 4 )\n )
}
そしてこれもまた無名関数のときと同様の方法で解決できます。 例えば以下のようにします。
functype FuncT( x int ) int
function dummy( x int )->{ return 0 }
{
varia alternative = 0
static varia factorial FuncT = dummy /* 一旦宣言を終らせる */
factorial = ^lam( x int ) int {
if x > 2 {
..x * factorial( x-1 ) /* OK: factorial は確実に宣言されている */
}
if x <= 0 {
..alternative
}
..x
}
final factorial /* factorialへの再代入はこれ以降禁止 */
Rrk_print( factorial( 4 )\n )
}
無名関数のときと同様、上記の処理に相当することを一気に行うシンタックスシュガーがあります。 無名関数ではこれをローカル関数と呼びましたが、lambda式ではこれをlambda関数と呼びます。 書式はにローカル関数におけるキーワードfunctionをlambdaに変えただけです。 例えば以下のようになります。
{
varia alternative = -1
lambda factorial( x int ) int {
if x > 2 {
..x * factorial( x-1 )
}
if x <= 0 {
..alternative
}
..x
}
Rrk_print( factorial( 4 )\n )
}
lambda関数では、演算子「^lam」ではなくキーワード「lambda」を使用しなければならないことに注意してください。 この実体は(ローカル関数と同様)finalizeされた関数型変数です。
lambda関数は式ではなく定義文です。
よってこれを他の式中に埋め込んだり、即時呼び出しすることは出来ません。
この関数型変数が有効なスコープは、このfactorialが宣言されたブロックが終るまでとなります。 またこの定義より前の位置でこれを呼び出すことはできません。 ただしlambda式/lambda関数内でリカーシブコールすることは可能です。
無名関数のときと同様、上の例でfactorialの戻り値の型として以下のように型隠蔽演算子「->」を使用することはできません。
「->」による戻り値の型推論では、一番最初に来るreturn文で指定された式の型を解析します。 この例ではreturn文にfactorialが再帰的に含まれているため、解析がそこで中断します。 例えば以下のように「..alternative」の方を先に持ってくればOKです。
{
varia alternative = -1
lambda factorial( x int )->{
if x > 2 {
..x * factorial( x-1 ) /* コンパイルエラー: この部分の型が推論できない */
}
if x <= 0 {
..alternative
}
..x
}
Rrk_print( factorial( 4 )\n )
}
「->」による戻り値の型推論では、一番最初に来るreturn文で指定された式の型を解析します。 この例ではreturn文にfactorialが再帰的に含まれているため、解析がそこで中断します。 例えば以下のように「..alternative」の方を先に持ってくればOKです。
{
varia alternative = -1
lambda factorial( x int )->{
if x <= 0 {
..alternative /* OK: alternativeの型は既知 */
}
if x > 2 {
..x * factorial( x-1 )
}
..x
}
Rrk_print( factorial( 4 )\n )
}
無名関数やlambda式を関数の戻り値として返す
無名関数は関数の戻り値としてreturn文に直接記述することもできます。
C言語をご存知の方は、(細かい違いは考えずざっくり言えば)これは関数ポインタを返す関数です。
ただC言語では無名関数のような表記はできませんので、別途関数の実体を別の場所に記述しておく必要はあります。
ただしRarakuにおいて無名関数を戻り値として指定する意味はほとんどありません。 それにも関わらずここでまずこれを説明するのは、この次に述べるlambda式を戻り値として指定する場合ではそれなりに意味があり、 それへの準備としてまずこのような表記に慣れておくためです。
それでは以下の例をご覧ください。
functype FuncT_Action() void
/* FuncT_Action型を返す関数 */
function get_bark() FuncT_Action
{
/* return文の戻り値として無名関数を直接指定 */
return ^fun(){
Rrk_print( "Bow wow!\n" )
}
}
varia func_var = get_bark() /* func_var は FuncT_Action型変数となる */
func_var()
上の例の関数get_barkの戻り値の型はFuncT_Action型(戻り値も引数も無しの関数型)となっています。 また関数内のreturn文ではそれと同じ型の無名関数を返しています。 すぐ下の「get_bark()」の呼び出しにおいては、この戻り値が func_var 変数に代入され、 最後に「func_var()」により、get_barkの戻り値において指定した無名関数が呼び出されることになります。
これを実行した結果を以下に示します。
Bow wow!
またほとんど同じことですが、上の例は以下のように記述することもできます。
functype FuncT_Action() void
/* FuncT_Action型を返す関数 */
function get_bark() FuncT_Action
{
/* return文の戻り値として無名関数を直接指定 */
return ^fun(){
Rrk_print( "Bow wow!\n" )
}
}
get_bark()()
一つ前の例との違いは、最後のget_barkの呼び出しの部分です。 今回は「get_bark()()」といった表記になり、「()」が二つ連続しています。 一つ目の「()」は「get_bark()」を実行するためのものです。 二つ目の「()」は「get_bark()」を実行した結果をさらに呼び出すためのものです。 「get_bark()」を実行した結果、戻り値は get_bark関数内のreturn文において指定した無名関数ですから、 それが呼び出される形になります。
「get_bark()()」という表記は「(get_bark())()」と書いたのと全く同じです。
これを実行した結果を以下に示します(一つ前の例と全く同じです)。
Bow wow!
ところで戻り値の型のFuncT_Actionですが、毎回この型をfunctype文で宣言し、関数の戻り値の型として指定するのも面倒ではないでしょうか? 実はこのような場合こそ型隠蔽演算子「->」が便利に働きます。 上の例をこれを用いて書き直すと以下のようになります。
function get_bark()->
{
/* return文の戻り値として無名関数を直接指定 */
return ^fun(){
Rrk_print( "Bow wow!\n" )
}
}
varia func_var = get_bark()
func_var()
get_bark()()
上記ではfunctype文が消え、FuncT_Actionという型を宣言する必要がなくなります(あたかも型FuncT_Actionが隠蔽されたかのような効果になります)。 なぜなら最初のreturn文で「^fun(){」という無名関数が指定されていますが、 この式から戻り値が「引数無し戻り値無しの関数型」であることが推論できるからです。 また下にある変数func_varも同じ型であるはずですからこのようにして連鎖的に型が決まります。
ここまでの例では、無名関数がその外側の関数のローカル変数を参照していませんでした。 これまで何度も述べた通り、無名関数の中からではその外側の関数内のローカル変数を参照することはできません。 例えば以下はコンパイルエラーとなります。
functype FuncT_ArgInt( x int ) int
function powInt( x int, y int ) int
{
.. x^^y
}
function powInt_curry( y int ) FuncT_ArgInt {
return ^fun( x int ) int { /* 無名関数を返す */
.. powInt( x, y ) /* コンパイルエラー : 無名関数ではこのyの参照は許可されない */
}
}
const powInt_toThe2 = powInt_curry( 2 ) /* 中身の無名関数を取得してこれを呼び出したい */
Rrk_print( "3^^2=" powInt_toThe2( 3 ) \n )
初めて見るとかなり奇異に感じられる例かもしれません。 無名関数「^fun( x int ) int」の型と FuncT_ArgInt型は全く同じであることに注意してください。 また無名関数「^fun( x int ) int」内で y にアクセスしようとしていますが、 この y は一つ上のpowInt_curry関数が所有するローカル変数であるため、この y へのアクセスは認められません。
ここで上の例において、無名関数の替わりに(全く同じ型、つまりFuncT_ArgInt型の)lambda式にしてみます。 つまり「return ^fun」となっているところを、以下のように「return ^lam」に修正してみます。
functype FuncT_ArgInt( x int ) int
function powInt( x int, y int ) int
{
.. x^^y
}
function powInt_curry( y int ) FuncT_ArgInt {
return ^lam( x int ) int { /* lambda式を返す */
.. powInt( x, y ) /* OK : lambda式ではこのyは参照可能 */
}
}
const powInt_toThe2 = powInt_curry( 2 ) /* 中身のlambda式を取得してこれを呼び出したい */
Rrk_print( "3^^2=" powInt_toThe2( 3 ) \n )
今度はコンパイルエラーとはならず問題ありません。 つまり無名関数のときには許可されなかった y へのアクセスが認められます。
そしてこれにより、powInt_curry( 2 ) で返されるlambda式は、(このとき y は2ですから)与えた引数を2乗するような関数としてふるまいます。 今これを powInt_toThe2 という変数に代入していますから、最終的に powInt_toThe2( 3 ) は 3の2乗、すなわち9を返すことになります。
この powInt_curry 関数で行っているような処理をカリー化と呼びますが、 わざわざこんなことをする意味については次の項で詳しく述べます。
勿論、上記も型隠蔽演算子「->」で書くことができます(これを使うと簡潔にはなりますが型が隠蔽される分状況が掴み辛いため、
上記では一旦記述を避けました)。
以下の通りになります。
function powInt( x int, y int ) int
{
.. x^^y
}
function powInt_curry( y int )->{
return ^lam( x int )->{ /* lambda式を返す */
.. powInt( x, y ) /* OK : lambda式ではこのyは参照可能 */
}
}
const powInt_toThe2 = powInt_curry( 2 ) /* 中身のlambda式を取得してこれを呼び出したい */
Rrk_print( "3^^2=" powInt_toThe2( 3 ) \n )
重要なのは powInt_toThe2 は powInt_curryより外にある変数にも関わらず、その powInt_curry 内の引数 y (今回の場合その値は2)が内部状態として保持されるということです。 powInt_curry の呼び出しはもう終了しているにも関わらず、その引数 y の実体はまだ生きているといった特殊な状況が発生します。 変数 powInt_toThe2 は、いわば関数としての性質と y(=2)というデータを内部に保持できる構造体としての性質の両方を兼ね備えた特殊なオブジェクトになります。 これを単に「powInt_toThe2(3)」と呼び出した場合、内部に保持された値2を加味しつつ、 同時に引数として3が与えられた関数としての挙動をとることになります(この場合は2の3乗を返します)。
「無名関数を返す処理」と「lambda式を返す処理」との決定的な違いは、
後者の場合、このようなデータを内部に保持できる構造体としての性質を持つことです。
このようなものを、単なる関数と区別するため関数オブジェクトと呼ぶこともあります
(Rarakuの内部実装であるlibRrkでは、これはRrkClosという名前の構造体になっています)。
これは関数と変数がセットになって一つの塊になって結びついているという意味で、 オブジェクト指向におけるプライベートなメンバ変数を持つクラスともよく似ています (実際、この性質を使って、オブジェクト指向のクラスにおけるプライベートメンバを実現することもできますが、 それについては「モジュールと情報隠蔽」のセクションで詳述します)。
この関数内で行われていることは(そのような内部データの外部へのエクスポートを伴う分)、単なる関数のネストとも異なります。 C言語で言えば、「^fun演算子で定義された無名関数を返すこと」は「関数のポインタを返すこと」に相当しますが、 一方で「^lam演算子で定義されたlambda式を返すこと」は「関数のポインタの役割も兼ねた特殊な構造体を返すこと」に相当します。 そして前者と区別するため、後者のような特殊なネストを持つ関数を高階関数と呼ぶこともあります。
勿論、このような機構を実現するためには(親となる)関数自体の寿命を終えた後も、そのローカル変数群の寿命を別途適切に管理する必要があります。 親となる関数の束縛から解放されるという意味でこれを自由変数と呼ぶこともあります。 自由変数をどのような手法で管理するかは、メモリプールやGCなど色々考えられますがその言語の実装にもよります。 因みにRarakuの場合、リファレンスカウンター方式のGCで管理する手法をとっています。 即ちlambda式によって確保された関数オブジェクトが誰からも参照されなくなった時点で、 それが保持する内部のローカル変数群の寿命も同時に終了する形になります。
これは関数と変数がセットになって一つの塊になって結びついているという意味で、 オブジェクト指向におけるプライベートなメンバ変数を持つクラスともよく似ています (実際、この性質を使って、オブジェクト指向のクラスにおけるプライベートメンバを実現することもできますが、 それについては「モジュールと情報隠蔽」のセクションで詳述します)。
この関数内で行われていることは(そのような内部データの外部へのエクスポートを伴う分)、単なる関数のネストとも異なります。 C言語で言えば、「^fun演算子で定義された無名関数を返すこと」は「関数のポインタを返すこと」に相当しますが、 一方で「^lam演算子で定義されたlambda式を返すこと」は「関数のポインタの役割も兼ねた特殊な構造体を返すこと」に相当します。 そして前者と区別するため、後者のような特殊なネストを持つ関数を高階関数と呼ぶこともあります。
勿論、このような機構を実現するためには(親となる)関数自体の寿命を終えた後も、そのローカル変数群の寿命を別途適切に管理する必要があります。 親となる関数の束縛から解放されるという意味でこれを自由変数と呼ぶこともあります。 自由変数をどのような手法で管理するかは、メモリプールやGCなど色々考えられますがその言語の実装にもよります。 因みにRarakuの場合、リファレンスカウンター方式のGCで管理する手法をとっています。 即ちlambda式によって確保された関数オブジェクトが誰からも参照されなくなった時点で、 それが保持する内部のローカル変数群の寿命も同時に終了する形になります。
カリー化
前の項の例を以下に再掲します。 ただし今回は「->」記法を用いています。
function powInt( x int, y int ) int
{
.. x^^y
}
function powInt_curry( y int )->{
return ^lam( x int )->{ /* lambda式を返す */
.. powInt( x, y ) /* OK : lambda式ではこのyは参照可能 */
}
}
const powInt_toThe2 = powInt_curry( 2 )
Rrk_print( "3^^2=" powInt_toThe2( 3 ) \n )
多分これだけですとわざわざこのようにする意義が全くわからないと思います。 このような処理をする理由は、これにより結果的に引数を一つ消去できるからです。 powIntは元々「引数を二つとる関数」でしたが、powInt_curry関数によって「引数を一つ返すlambda式」を取得することができます。
今最終的に欲しいのは「powInt_curry関数」そのものではなく、「powInt_curryの戻り値となるlambda式」の方です。
ただし実際にカリー化を行うために呼び出す関数はやはり「powInt_curry関数」となります。
このあたりはややこしいので注意しましょう。
これを逐次繰り返せば、例えば引数が数個ある複雑な関数をよりシンプルな関数(最終的には引数を一つだけとる関数)へどんどん変換することもできます。 このような処理には名前がついており、一般的にはカリー化と呼ばれます。
本来カリー化(currying)とはもっと数学的に厳密に定義された概念ですが、ここではそこまでは考えないことにしましょう。
カリー化によりコールバック関数登録の適用幅を広げるようなことができます。 このことは裏を返せば、理想的にはコールバック関数の型は「引数を一つとる関数」だけを想定しておけばよいということになります。 なぜならカリー化により「引数を数個とる関数」であっても「引数を一つとる関数」へ変換し、そのようなコールバックとして登録するといったことができるからです(ただし現実的にはこのようなカリー化を避けた方がむしろ簡潔になる場合も往々にしてあります)。
実際の例で見てみましょう。 以降で述べる例は、大雑把に概要を述べれば整数の配列iaryの各要素を2乗、3乗、4乗した新しい配列を取得しようとするものです。 指数の配列はdim_aryに格納されているものとし、結果はans_aryという2次元配列に格納することにします。
まず対比のため、カリー化を使わない愚直な例から見てみます (わざわざ対比して確認するのは、時には愚直な実装の方が望ましい場合もあり得るからです)。
/* 提供されている関数群 */
array IAry int[]
functype FuncT_ArgInt( x int ) int
function powInt( x int, y int )->
{
..x^^y
}
function mapIntAry( f FuncT_ArgInt, iary int[] )->
{
varia ans int[]
foreach i : iary {
Rrk_push_bk( ans, f( i ) )
}
..ans
}
function func_byDirectAdapter( dim_ary int[], iary int[], varia ans_ary IAry[] )
{
/* アダプタを愚直にそれぞれ作り、それらをmapIntAryに適用させる方法 */
function powInt_toThe2( x int )->{
..powInt( x, 2 )
}
function powInt_toThe3( x int )->{
..powInt( x, 3 )
}
function powInt_toThe4( x int )->{
..powInt( x, 4 )
}
Rrk_push_bk( ans_ary, mapIntAry( powInt_toThe2, iary ) )
Rrk_push_bk( ans_ary, mapIntAry( powInt_toThe3, iary ) )
Rrk_push_bk( ans_ary, mapIntAry( powInt_toThe4, iary ) )
}
/* 入力データと結果表示 */
const dim_ary int[] = [ 2, 3, 4 ]
const iary int[] = [ 1, 2, 3, 4, ]
varia ans_ary IAry[]
func_byDirectAdapter( dim_ary, iary, ans_ary )
function printIntAry( label conststr, iary int[] ) void {
Rrk_print( label "=[" )
foreach i : iary {
Rrk_print( i \, )
}
Rrk_print( "]\n" )
}
printIntAry( "dim2:", ans_ary[0] )
printIntAry( "dim3:", ans_ary[1] )
printIntAry( "dim4:", ans_ary[2] )
上の例で、printIntAry 以下は単なる結果表示のための処理です。 実際にコアとなる処理を行っているのがfunc_byDirectAdapterになります。 ここではmapIntAryにpowInt関数をコールバック関数として与え、各配列要素についてそれを適用し、 最終的な結果をans_ary内に生成してもらいたいとします。
ただしmapIntAryで受け取ることのできるコールバック関数は FuncT_ArgInt であり、これは引数を一つだけとることのできる関数です。 今、powIntは引数を二つとる関数ですからこのままでは型が一致せず、mapIntAry関数へ指定できません。 そこで上の例では、powInt_toThe2( x int )、powInt_toThe3( x int )、powInt_toThe4( x int ) というように、 (各べき乗の値毎に)関数の引数が1つとなるようないわばアダプタを作り、それらをmapIntAryへ渡しています。
勿論、powInt_toThe2( x int )、powInt_toThe3( x int )、powInt_toThe4( x int ) を無名関数にして
mapIntAryに直接与えることもできます。
この例の実行結果は以下のようになります。
dim2:=[1, 4, 9, 16, ]
dim3:=[1, 8, 27, 64, ]
dim4:=[1, 16, 81, 256, ]
では次に全く同じ処理で、カリー化を使った例を見ます。
/* 提供されている関数群 */
array IAry int[]
functype FuncT_ArgInt( x int ) int
function powInt( x int, y int )->
{
..x^^y
}
function mapIntAry( f FuncT_ArgInt, iary int[] )->
{
varia ans int[]
foreach i : iary {
Rrk_push_bk( ans, f( i ) )
}
..ans
}
function func_byCurry( dim_ary int[], iary int[], varia ans_ary IAry[] )
{
/* アダプタを作るための無名関数(powInt_curry)を利用してアダプタを自動生成し、それをmapIntAryに適用する方法 */
function powInt_curry( y int )->{ /* カリー化 */
return ^lam( x int )->{ /* lambda式 */
..powInt( x, y )
}
}
foreach dim : dim_ary {
const ans = mapIntAry( powInt_curry(dim), iary )
Rrk_push_bk( ans_ary, ans )
}
}
/* 入力データと結果表示 */
const dim_ary int[] = [ 2, 3, 4 ]
const iary int[] = [ 1, 2, 3, 4, ]
varia ans_ary IAry[]
func_byCurry( dim_ary, iary, ans_ary )
function printIntAry( label conststr, iary int[] ){
Rrk_print( label "=[" )
foreach i : iary {
Rrk_print( i \, )
}
Rrk_print( "]\n" )
}
printIntAry( "dim2:", ans_ary[0] )
printIntAry( "dim3:", ans_ary[1] )
printIntAry( "dim4:", ans_ary[2] )
今度は実際にコアとなる処理を行っているのがfunc_byCurryになりますが、先ほどの例との違いは、 powInt_curry関数によってアダプタを自動的に生成していることです。 つまり「powInt_curry(dim)」の呼び出しによって、先ほどの例で言う powInt_toThe2( x int )、powInt_toThe3( x int )、powInt_toThe4( x int ) といったアダプタを自動的に生成し、それをmapIntAryへ渡しています。 コードの簡潔さとしては先ほどとそう変わらないか、むしろこちらの方が捻くれて複雑なように見えるかもしれませんが、 実際はアダプタの直書きがない分、一段階柔軟性のあるコードになっています。 iaryやdim_aryの変更があったとしても、コードの修正はその部分だけで済みます。
ちなみにfunc_byCurry関数内において、powInt_curryを作ってそれを呼び出す替わりに
中身のlambda式をmapIntAryの引数に直接与えることもできます。
func_byCurry関数だけを抜粋しますが、つまり以下のように書いても結果は同じです。
function func_byCurry( dim_ary int[], iary int[], varia ans_ary IAry[] )
{
foreach dim : dim_ary {
const ans = mapIntAry(
^lam( x int )->{ /* lambda式 */
..powInt( x, dim )
}, /* powInt_curry(dim), の替わりにlambda式を直接指定 */
iary )
Rrk_push_bk( ans_ary, ans )
}
}
さて、ここでカリー化の説明を終えてもいいのですが、 そもそもこんなものを使わなくてもforeachをネストすれば同様の処理が書けるわけです。 そこでそのように書いた場合との比較もしておきましょう。
/* 提供されている関数群 */
array IAry int[]
functype FuncT_ArgInt( x int ) int
function powInt( x int, y int ) int
{
..x^^y
}
function mapIntAry( f FuncT_ArgInt, iary int[] ) int[]
{
varia ans int[]
foreach i : iary {
Rrk_push_bk( ans, f( i ) )
}
..ans
}
function func_byForeachNest( dim_ary int[], iary int[], varia ans_ary IAry[] ) void
{
/* foreachのネストで直接実装 */
foreach dim : dim_ary {
/* mapIntAry の実装をわずかに変えたものを直に書く */
varia ans int[]
foreach i : iary {
Rrk_push_bk( ans, powInt( i, dim ) )
}
Rrk_push_bk( ans_ary, ans )
}
}
/* 入力データと結果表示 */
const dim_ary int[] = [ 2, 3, 4 ]
const iary int[] = [ 1, 2, 3, 4, ]
varia ans_ary IAry[]
func_byForeachNest( dim_ary, iary, ans_ary )
function printIntAry( label conststr, iary int[] ) void {
Rrk_print( label "=[" )
foreach i : iary {
Rrk_print( i \, )
}
Rrk_print( "]\n" )
}
printIntAry( "dim2:", ans_ary[0] )
printIntAry( "dim3:", ans_ary[1] )
printIntAry( "dim4:", ans_ary[2] )
今回は実際にコアとなる処理を行っているのがfunc_byForeachNestになりますが、 この関数ではそもそもmapIntAryを使用せず、それと同様の処理をforeach文で記述しています。
実際のところこの程度の複雑さの例であれば、この最後の書き方が一番素直でわかりやすいのではないかと思います。 というのもfunc_byForeachNest内のネストの最内にある「Rrk_push_bk( ans, powInt( i, dim ) )」を見れば中で何が行われているのか一目瞭然であり、 結局前二つのコールバックを使った例よりも何を行っているのか直感的にわかりやすいです (しかもlambdaを使っていない分、実行速度もこちらの方が多分高速です)。
しかしこれよりもさらに複雑な状況になると、また話は変わってきます。 例えば以下のような項目をすべて満たすような場合です。
- mapIntAryに相当する処理がもっと複雑である。
- mapIntAryに相当する処理がもっと色々な場所で必要になる。
- mapIntAryのコールバック関数部分に与えるべき処理がもっと多様である。
このような場合、コールバック関数を使わず同様の処理をコードの至るところに書くと相当大変なことになります。 かといってコールバック関数を使おうにもその関数型が合わないといった問題にぶつかることになります。 そのような場合がまさにカリー化を使うタイミングかと思います。
ただそうは言ってもそこまで複雑な状況は、ある程度の規模以上のプログラムではないとそれほど遭遇しない気もします。 その意味で、カリー化の使用は(関数型言語であれば勿論話は別ですが)Rarakuにおいてはどちらかといえば慎重に見極めた上で、 どうしても必要な場合に限った方がよいと思います。
目次に戻る
ファイル入出力とネイティブオブジェクト
ファイルを扱うためのネイティブオブジェクト
OSが管理するデバイスやファイル等の資源にアクセスする場合、システムが提供する専用の関数でそれらをのデバイス/資源をオープンし、 そのIDを取得する必要があります。そのようなIDの役割を果たす値の型のうち、GCで管理されたものをRaraku上ではネイティブオブジェクトと呼んでいます。
ネイティブオブジェクトはconststr型と似ており、モディファイアのアサイナビリティの規則もconststr型と同様です。
ただしconststrと違い大小比較といったことは出来ず、「==」「!=」演算子による比較演算のみが可能です。
ネイティブオブジェクトもまたこれまでの変数/定数と同様varia/const文により宣言できます。 ネイティブオブジェクト型変数/定数には null を格納することもでき、特に変数宣言において明示的な初期化をしない場合はデフォルトでnullが格納されます。
この点もconststrと異なります。
conststrで初期化をしなかった場合は、デフォルトでnullではなく空文字("")が格納されるからです。
Rarakuが標準で提供しているネイティブオブジェクト型の一つにRrkFileがあります。 これはrrk_pkg/std/file.rrkhに定義されていますのでこれをインポートする必要があります。 以下ではこれを使った例を見てみます。
import std/file
const fp RrkFile^? = RrkFile_open( "test.txt", "rb" )
if fp {
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
RrkFile_close( fp )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
一行目の「import std/file」によりrrk_pkg/std/file.rrkhをインポートしています。 次のRrkFile_openは第1引数で与えた名前のファイルをオープンするための関数です。 第2引数の"rb"はそのファイルを「読み込みモード」で開くことを意味します。
C言語をご存知の方はfopen関数における指定と意味は同じです。
この例ではわかりやすさのため、敢えてfpの宣言部に型名としてRrkFileを付けていますが、
実際には以下のようにRrkFileを省略して記述することもできます。
これはRrkFile_openがRrkFileを返す関数であることから、const文の型が型推論できるためです。
const fp = RrkFile_open( "test.txt", "rb" )
この関数によりそのファイルに関連付けられたネイティブオブジェクトを生成することができます。 以降の処理はこのネイティブオブジェクトを介して、関連付けられたファイルに対する様々な処理を行うといった流れになります。
ただしこの関数は失敗する場合もあることに注意してください。 その戻り値は、指定したファイルのオープンに成功した場合はそのファイルに関連付けられたネイティブオブジェクトとなりますが、 逆にそのオープンに失敗した場合 null となります。そのためこの関数も戻り値の型はnullableであり「RrkFile^?」となります。
ファイルのオープンに失敗する例を念のため挙げておきます。
例えば読み込みモードでファイルを開き、尚且つそこで指定したファイル名のファイルが実際には存在しない場合は
そのファイルのオープンに失敗します。
この例では関数RrkFile_openの戻り値をネイティブオブジェクト型変数 fp に代入しています。 またその直後でRrkFile_openの成否を判定するため、if 文で fp が null であるか否かを調べています。 null でなかった場合、ファイルのオープンに成功していますので if 側のブロックを実行します。 逆に null であった場合、ファイルのオープンに失敗していますので else 側のブロックを実行し、 エラーメッセージを出力します。
今、ファイルのオープンが成功し、if 側のブロックが実行されるとしましょう。 このブロック内の RrkFile_getLine( fp, line ) は、テキストファイルから1行ずつ読み込むための標準関数です。 ここでは fp が指し示すファイルの中身をテキストファイルとみなしてそれを行単位で読み込み、 その内容をlineに格納しています。 このRrkFile_getLine は現在の行の読み込みに成功した場合は true を返し、次の行にフォーカスを移動します。 このようにして一番初めの行から順番に読み続け、ファイルの終端に至ると false を返します。 つまりこのとき上記のwhile文も終了します。
その後、Rrk_print( text ) によりその値を表示しています。 while文の内部ではそのファイルの全行がtextへと連結されましたので、 最終的にtextにはそのファイルの全文が格納されているはずです。
そのファイルに関する処理がすべて終わった場合、オープンしたファイルはクローズしなければなりません。 それを行っているのがRrkFile_close( fp )となります。
ただし上記の例の場合、最悪RrkFile_close( fp )の呼び出しを忘れたとしても そのネイティブオブジェクトがどこからも参照されなくなった時点でGCによって ファイルのクローズとそのネイティブオブジェクト自体のメモリ解放が自動的に行われます。
既にRrkFile_closeを呼び出してファイルをクローズしている場合は、
GCによるファイルクローズ処理はもう行われません。
この場合、GCによって行われるのはそのネイティブオブジェクト自体のメモリ解放のみとなります。
一方、RrkFile_closeはファイルのクローズだけを行い、 ネイティブオブジェクト fp 自体のメモリを解放するわけではありません。 fp 自体のメモリはあくまでGCによって解放されます。 そのため、この fp を(RrkFile_close実行後に)例えば他の構造体や配列に記録したり、Rrk_printで表示することは問題ありません。
一方、RrkFile_closeはファイルのクローズだけを行い、 ネイティブオブジェクト fp 自体のメモリを解放するわけではありません。 fp 自体のメモリはあくまでGCによって解放されます。 そのため、この fp を(RrkFile_close実行後に)例えば他の構造体や配列に記録したり、Rrk_printで表示することは問題ありません。
よって、今回の例の場合RrkFile_closeを呼び出すことは必須とまでは言えないことになりますが、 しかし使われなくなった時点で明示的にRrkFile_closeを呼び出す方が ファイル入出力の有効範囲をコード上で明確にすることができます。 また、あまりないことかもしれませんが、例えば循環参照が絡んだ構造体の中にfpが格納された場合、 fpに対してGCが自動的には作用せず、結果的にファイルクローズせずに残るといった状況は想定できます。 よってGCによる自動ファイルクローズはあくまで保険的なものと捉え、 基本的には明示的にRrkFile_closeは呼び出すものだと考えた方がよいでしょう。
同じfpに対してRrkFile_closeを二回以上呼び出すことは特に問題なく、その場合は二度目以降の呼び出しは何も行われません。
またRrkFile_close( fp )を実行した後で、仮にRrkFile_getLine( fp, line )といった呼び出しを行った場合も
やはり何も行われません。
C言語をご存知の方は、内部的にはネイティブオブジェクトという(C言語の意味での)構造体があり、
それが(C言語の意味での)ファイルポインタ c_fp をメンバとして包含している状態であると考えて差し支えありません
(実際、そのような形で実装しています)。
RrkFile_close( fp )を実行すると、そのfp内部のc_fpに対してfclose( c_fp )が実行され、さらにc_fpに(C言語の意味での)NULLがセットされます。 そのため、その後に fp が使われるような関数の呼び出し(RrkFile_close( fp ) や RrkFile_getLine( fp, line )など)が発生しても 既に内部のc_fpがNULLであることが目印となって、何も行われないようになっています。
ただし念のために補足しますと、これは内部の(C言語の意味での)ファイルポインタc_fp がNULLになっただけであって、 ネイティブオブジェクトfp自体が(Rarakuの意味での)null値になったわけではありません。 従ってRrkFile_close実行後に、例えば fp をif文で判定した結果は(依然としてfpは非nullですので)trueとなります。
RrkFile_close( fp )を実行すると、そのfp内部のc_fpに対してfclose( c_fp )が実行され、さらにc_fpに(C言語の意味での)NULLがセットされます。 そのため、その後に fp が使われるような関数の呼び出し(RrkFile_close( fp ) や RrkFile_getLine( fp, line )など)が発生しても 既に内部のc_fpがNULLであることが目印となって、何も行われないようになっています。
ただし念のために補足しますと、これは内部の(C言語の意味での)ファイルポインタc_fp がNULLになっただけであって、 ネイティブオブジェクトfp自体が(Rarakuの意味での)null値になったわけではありません。 従ってRrkFile_close実行後に、例えば fp をif文で判定した結果は(依然としてfpは非nullですので)trueとなります。
上記の例では、RrkFile_getLineにより取得したlineをtextという一つの大きな文字列に連結していましたが、
これを文字列の配列にプッシュしたい場合は注意が必要です。
例えば以下をご覧下さい。
この例では、文字列の配列line_aryを宣言し、 RrkFile_getLineにより取得したlineをline_aryに格納しています。 この場合、Rrk_push_bkではなく、替わりにRrk_push_bk_cstrを使わなければなりません。
lineの値はline_aryの要素へディープコピーしなければなりませんが、Rrk_push_bkの場合はシャローコピーされてしまいます。 シャローコピーは単にlineの実体を指し示すだけですから、このようにするとline_aryのすべての要素が同じlineの実体を指し示すことになり、 意図しない結果となるでしょう (上記の場合、RrkFile_getLineで最後に取得した値がline_aryの全ての要素に格納されているように見える結果になると思います)。 一方、Rrk_push_bk_cstrの場合、これがディープコピーされるため、意図した通りの結果になります。
import std/file
varia line_ary string[]
const fp RrkFile^? = RrkFile_open( "test.txt", "rb" )
if fp {
varia line string
while RrkFile_getLine( fp, line ){
Rrk_push_bk_cstr( line_ary, line ) /* 注意:Rrk_push_bkは使わないこと! */
}
RrkFile_close( fp )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
/* line_aryを使った処理 */
この例では、文字列の配列line_aryを宣言し、 RrkFile_getLineにより取得したlineをline_aryに格納しています。 この場合、Rrk_push_bkではなく、替わりにRrk_push_bk_cstrを使わなければなりません。
lineの値はline_aryの要素へディープコピーしなければなりませんが、Rrk_push_bkの場合はシャローコピーされてしまいます。 シャローコピーは単にlineの実体を指し示すだけですから、このようにするとline_aryのすべての要素が同じlineの実体を指し示すことになり、 意図しない結果となるでしょう (上記の場合、RrkFile_getLineで最後に取得した値がline_aryの全ての要素に格納されているように見える結果になると思います)。 一方、Rrk_push_bk_cstrの場合、これがディープコピーされるため、意図した通りの結果になります。
natvobj文
ネイティブオブジェクト型はnatvobj文を用いて「natvobj ネイティブオブジェクト型名」といった書式で宣言されます。
例えばRarakuがデフォルトで提供しているRrkFile型は rrk_pkg/std/file.rrkh 内で以下のように宣言されています。
natvobj RrkFile
その他、Rarakuがデフォルトで提供しているRrkRgx型は rrk_pkg/std/rgx.rrkh 内で以下のように宣言されています。
natvobj RrkRgx
RrkFileとRrkRgxはどちらもネイティブオブジェクト型の一種ではありますが、その型名が異なるため両者に互換性はありません。 つまり前者を後者に代入したりその逆を行うことはできません(そのような記述をするとコンパイルエラーとなります)。
ネイティブオブジェクト型は通常、Rarakuが提供するネイティブ関数の戻り値を受けるために使用されることが多いです。 そのためプログラマがnatvobj文を使ってネイティブオブジェクト型を独自に定義/宣言することはあまりないかもしれません。
ちなみにRarakuではネイティブ関数を自作することもできますが、
その方法はこの入門編のレベルを超えますので説明は割愛します。
尚、これ以降、ネイティブオブジェクト型のことを少し短くnatvobj型と表記することがありますが、両者は同じものです。
natvobj型をif文等の条件式や論理演算子に適用した場合の簡略記法
一般にbool型変数にnatvobj型の値を代入することはできませんが、 if文などの条件分岐やwhile文などのループ文における条件式、あるいは論理演算子(「&&」「||」「!」)では、 例外的にnatvobj型を単独で指定できます(これは整数などの場合と同様です)。
このとき、そのnatvobj型変数がnull以外の値に等しいならば true、null値に等しいならば false と評価されます。 例えば以下の例をご覧ください。
import std/file
const fp = RrkFile_open( "test.txt", "rb" ) /* fp は RrkFile natvobj型 */
if fp { /* if notnull fp { と記述したのと全く同じ */
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
RrkFile_close( fp )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
ここで「if fp {」と記述した部分は「if notnull fp {」と全く同じ意味になります。 ちなみに「if !fp {」と記述した場合は「if fp == null {」と同じになります。
その他「if fp1 && fp2 {」、「if fp1 || fp2 {」などと記述することも可能で、 その場合も各natvobj型変数がnull値に等しいか否かで同様にbool値として評価されます。
尚、この簡略記法と「:=」を用いた記法を組み合わせて以下のように記述することもできます。
import std/file
if fp := RrkFile_open( "test.txt", "rb" ); fp {
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
RrkFile_close( fp )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
さて、ここまでの例では、使い終わったファイルのネイティブオブジェクトをクローズするために RrkFile_close( fp ) を一番最後に呼び出していました。 これを忘れずに呼び出すなら何の問題もないのですが、 しかし一番最後という位置は書き忘れを生じやすいものです。
もっとも、繰り返しになりますがネイティブオブジェクトでは多くの場合、最後に明示的にRrkFile_closeを呼び出さなくても
(それが誰からも参照されなくなった時点で)自動的にファイルはクローズされますが、
基本的にはこれを一番最後に呼び出さなければならないものと考えましょう。
実際のプログラミングでは RrkFile_close はdefer文といっしょに使うのが望ましいでしょう。 これを使えば RrkFile_open を記述した位置とほとんど同じ位置にRrkFile_closeを記述でき、この方が書き忘れが生じにくくなります。 defer文については次のセクションで詳しく説明しますので、是非とも合わせてお読みください。
目次に戻る
defer文
defer文の基本
defer文は指定した関数呼び出しを「遅らせる」文で「defer 関数呼び出し」といった書式になります。 defer文を実行したタイミングではまだその関数は呼び出されません。 それが実際に呼び出されるタイミングは、関数内であればその関数のreturn時であり、 グローバルスコープであればそのグローバルスコープの終了時 (典型的にはプログラムそのものの終了時)となります。
これはgo言語におけるdefer文とよく似ています。
例えば以下の例をご覧ください。
defer Rrk_print( "good-bye\n" )
Rrk_print( "hello\n" )
Rrk_print( "world\n" )
上記では3つの関数呼び出しがありますが、最初のRrk_print文はdefer文の中で指定されており、 そのためこれだけが特別に一番最後に呼び出されます。 残り二つは通常通り、書いてある順に呼び出されます。 この例の実行結果は以下のようになります。
hello
world
good-bye
defer文を複数書いた場合は、defer文が実行されたのとは逆順にそれらが(関数の最後に)呼び出されます。
これはgo言語におけるdefer文と同様です。
例えば以下の通りです。
defer Rrk_print( "end1\n" )
Rrk_print( "hello\n" )
defer Rrk_print( "end2\n" )
Rrk_print( "world\n" )
この例の実行結果は以下のようになります。
hello
world
end2
end1
関数の中とグローバルスコープの両方で使った場合の例も見ておきましょう。 以下をご覧ください。
function func(){
defer Rrk_print( "func end\n" )
Rrk_print( "func1\n" )
Rrk_print( "func2\n" )
}
defer Rrk_print( "global end\n" )
Rrk_print( "global1\n" )
Rrk_print( "global2\n" )
func()
この例の実行結果は以下のようになります。
global1
global2
func1
func2
func end
global end
「解放する関数」を「生成する関数」のそばに書く
defer文がどのようなものであるかはわかりましたが、これはどのような場合に使うのでしょう? 例えば何かを解放する系の関数を使うような場合に便利です。
defer文を使わない場合、例えばファイル入出力では、以下のようにまずRrkFile_open関数でファイルをオープンし、 RrkFile_close関数でそのファイルを最後にクローズします。
import std/file
function example(){
const fp = RrkFile_open( "test.txt", "rb" )
if fp {
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
RrkFile_close( fp ) /* at last */
} else {
Rrk_print( "Cannot open text.txt\n" )
}
}
example()
ただこのようにファイルをオープンする関数とクローズする関数を離して書くと、 やはり後に来る関数の方を書き忘れる確率が高まります。 そのためプログラマはこれを書き忘れないよう細心の注意を払う心配があります。
一方、defer文を使う場合、オープンとクローズの関数をほぼ同じ位置に書くことができます。 例えば以下のようになります。
import std/file
function example(){
const fp = RrkFile_open( "test.txt", "rb" )
defer RrkFile_close( fp ) /* RrkFile_closeをRrkFile_openのそばに書いておく(ここではまだ実際には実行されない) */
if fp {
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
/* 実際にはこの位置で RrkFile_close( fp ) が呼び出される */
}
example()
RrkFile_close( fp ) の呼び出しを事前に予約しておくといったイメージです。 このようにした方が解放する関数を書き忘れる確率が低くなることが期待でき、 プログラマがその点に注意する労力を軽減できます。
RrkFile_close は指定されたfpがnullの場合は何の効果も持たない関数であることに注意しましょう。 上記ではRrkFile_openが失敗してfpがnullである場合でもRrkFile_close( fp ) が実行されますが、 それは別に問題ありません。
ただし、いくら労力が軽減できると言っても、今度はdefer文を書き忘れないように注意しなければならないことにはなります。
またそもそもこのようにOSの資源にアクセスするようなイベントはそれなりに大きな出来事ですから(とはいえファイル入出力程度なら通常はたいした出来事ではないですが)、
プログラミングの姿勢としてはこれを安易に自動処理させるべきではなく、
細心の注意を払いつつ解放する関数を明確な位置で呼ぶべきという考え方もわからなくはありません。
実際、C言語(C++ではなく)などではそのようにして記述することが求められますし、おそらくいざデバッグとなった段では そのように明確な位置に記述していた方が解析はしやすいと思います。 そしてその観点から言えばdefer文はさほど重要なものに見えないかもしれません。
しかし実はdefer文の意義は「プログラマが(この種の)書き忘れに注意する労力を軽減できる」ことだけではありません。 後のセクションで述べる大域脱出を行いたいような場合、defer文は別の意味で威力を発揮します。 大域脱出については、「大域脱出とtry文」のセクションで詳しく説明します。
もっとも、ネイティブオブジェクトである以上、大抵の場合、最後はGCによって自動的な処理がなされてしまうわけですが、
今はそれは置いておきます。
実際、C言語(C++ではなく)などではそのようにして記述することが求められますし、おそらくいざデバッグとなった段では そのように明確な位置に記述していた方が解析はしやすいと思います。 そしてその観点から言えばdefer文はさほど重要なものに見えないかもしれません。
しかし実はdefer文の意義は「プログラマが(この種の)書き忘れに注意する労力を軽減できる」ことだけではありません。 後のセクションで述べる大域脱出を行いたいような場合、defer文は別の意味で威力を発揮します。 大域脱出については、「大域脱出とtry文」のセクションで詳しく説明します。
ところで、「ファイル入出力とネイティブオブジェクト型」のセクションでも述べたとおり、 if文の条件部の中で fp の宣言を同時に行うことができます。 この記法とdefer文を組み合わせて、一つ前の例と全く同じ処理を以下のように記述することもできます。
import std/file
function example(){
if fp := RrkFile_open( "test.txt", "rb" ); {
defer RrkFile_close( fp ) /* RrkFile_closeをRrkFile_openのそばに書いておく(ここではまだ実際には実行されない) */
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
} else {
Rrk_print( "Cannot open text.txt\n" )
}
/* 実際にはこの位置で RrkFile_close( fp ) が呼び出される */
}
example()
Rarakuではほとんどのケースでは、この書き方が一番望ましいと思われます。
変数fpのスコープが気になる方もおられるかもしれません。
上記のようにif文の条件部の中でfpを宣言した場合、fpのスコープはif文のブロック内とelse文のブロック内に限られます。 そのため、上記ではdefer文をif文のブロック内の最初に記述しています。
一方、defer文で指定したRrkFile_closeが実際に呼び出されるのは一番最後(グローバルスコープではファイルの一番最後、関数内ではその関数が終了するタイミング)であり、 それはこのif文else文のブロックの外になる場合もあります。 本来であればこのときfpのスコープはもう終了しているはずですが、defer文で指定された関数の引数におけるfpに限り、 特別に寿命が引き伸ばされ(場合によっては自由変数化され)、上記の最後の位置での呼び出しにおいてこれが有効に機能します。 そのため、上記は問題なく実行できるわけです。
上記のようにif文の条件部の中でfpを宣言した場合、fpのスコープはif文のブロック内とelse文のブロック内に限られます。 そのため、上記ではdefer文をif文のブロック内の最初に記述しています。
一方、defer文で指定したRrkFile_closeが実際に呼び出されるのは一番最後(グローバルスコープではファイルの一番最後、関数内ではその関数が終了するタイミング)であり、 それはこのif文else文のブロックの外になる場合もあります。 本来であればこのときfpのスコープはもう終了しているはずですが、defer文で指定された関数の引数におけるfpに限り、 特別に寿命が引き伸ばされ(場合によっては自由変数化され)、上記の最後の位置での呼び出しにおいてこれが有効に機能します。 そのため、上記は問題なく実行できるわけです。
上記の例では、エラーメッセージ表示を一番後方のelseの中で行っています。 しかし、これをまず真っ先に片付けたいと思われる場合は以下のようにif文の外に書いてもよいかもしれません。
import std/file
function example() bool {
const fp = RrkFile_open( "test.txt", "rb" )
defer RrkFile_close( fp ) /* RrkFile_closeをRrkFile_openのそばに書いておく(ここではまだ実際には実行されない) */
/* エラーは先に片付けておく */
if !fp {
Rrk_print( "Cannot open text.txt\n" )
return false /* 実際にはこの位置で RrkFile_close( fp ) が呼び出される */
}
varia line string
varia text string
while RrkFile_getLine( fp, line ){
text &= line
}
Rrk_print( text )
return true /* 実際にはこの位置で RrkFile_close( fp ) が呼び出される */
}
example()
また、このあたりからは個人の好みの問題になってくるとは思いますが、特にファイルハンドラが複数あってしかも何だかの理由でそれを同時に使いたい状況では、 if文の外で記述した方がif文のブロックのネストの深化を防ぐことができる場合があります。 例えば以下の通りです。
import std/file
function example() bool {
const fp1 = RrkFile_open( "test_for_read.txt", "rb" ) defer RrkFile_close( fp1 )
if !fp1 {
Rrk_print( "Cannot open text1.txt\n" )
/***
* 実際にはこの位置で RrkFile_close( fp1 ) が呼び出される.
* defer RrkFile_close( fp2 )はまだ実行されていないため、RrkFile_close( fp2 )は呼び出されません.
*/
return false
}
/***
* RrkFile_openの第2引数に"wb"を指定した場合、このファイルがカレントディレクトリ新しく生成される.
*/
const fp2 = RrkFile_open( "test_for_write.txt", "wb" ) defer RrkFile_close( fp2 )
if !fp2 {
Rrk_print( "Cannot open text2.txt\n" )
return false /* 実際にはこの位置で RrkFile_close( fp1 )とRrkFile_close( fp2 ) が呼び出される */
}
varia line string
while RrkFile_getLine( fp1, line ){
/* fp1(test_for_read.txt)からfp2( test_for_write.txt )へ一行ずつ書き込む */
RrkFile_print( fp2, line )
}
return true /* 実際にはこの位置で RrkFile_close( fp1 )とRrkFile_close( fp2 ) が呼び出される */
}
example()
上記では RrkFile_open と defer RrkFile_close を1行でまとめて書いていますが、実際にはこれは二つの文です。 基本どおり2行で分けて書いても全く構いませんが、これらは慣用句的にペアになる上、可読性に問題はないと判断して1行で書いています(単に好みの問題です)。 また 上記ではfp2のRrkFile_openを少し後で書いていますが、これらを最初に行っても特に問題ないでしょう(これも好みの問題です)。
一方、if文の条件式の中でRrkFile_openする場合、以下のようにdefer文をくっつけて記述することはできないことに注意してください。
import std/file
function example() bool {
if fp := RrkFile_open( "test.txt", "rb" ) defer RrkFile_close(fp); { /* コンパイルエラー : if文の「;」の前半に複数の文は書けない */
/* something process */
}
}
example()
このような場合、必ずifブロックの中(通常はその中の最初)にdefer RrkFile_closeを記述する必要があります。 例えば以下の通りです。
function example() bool {
if fp := RrkFile_open( "test.txt", "rb" ); {
defer RrkFile_close(fp) /* OK */
/* something process */
return true
}
return false
}
example()
defer文に無名関数やlambda式などの呼び出しを指定する
defer文では、いわゆる通常の関数呼び出しのみならず、 無名関数やlambda式の即時呼び出しを指定することも可能です。 例えば以下のようになります。
function func()
{
varia str = "world"
/* defer文に無名関数の即時呼び出しを指定 */
defer ^fun( s conststr ){
Rrk_print( "anonymous " s \n )
}( "hello" )
/* defer文にlambda式の即時呼び出しを指定 */
defer ^lam(){
Rrk_print( "lambda " str \n )
}()
Rrk_print( "func1"\n )
Rrk_print( "func2"\n )
}
func()
この例の実行結果は以下のようになります。
func1
func2
lambda world
anonymous hello
また以下のように無名関数やlambda式を一旦関数型変数に格納し、それを介した呼び出しを指定することも可能です。 例えば以下のようになります。
function func()
{
varia str = "world"
varia f = ^fun( s conststr ){
Rrk_print( "anonymous " s \n )
}
varia l = ^lam(){
Rrk_print( "lambda " str \n )
}
defer f( "hello" )
defer l()
Rrk_print( "func1"\n )
Rrk_print( "func2"\n )
}
func()
この例の実行結果は以下のようになります(一つ前の例と全く同じです)。
func1
func2
lambda world
anonymous hello
またローカル関数やlambda関数の呼び出しを指定することも可能です。 それがたとえ再帰呼び出しを伴うようなものでも一応は構いません。 例えば以下のようになります。
function func()
{
varia str = "world"
function factorial_f( i int )->{
Rrk_print( "factorial_f : " \=i \n )
if i <= 1 {
..i
}
.. i * factorial_f( i-1 )
}
lambda factorial_l( i int )->{
Rrk_print( "factorial_l : " \=i \n )
if i <= 1 {
..i
}
.. i * factorial_l( i-1 )
}
/* defer文にローカル関数を指定 */
defer factorial_f( 4 )
/* defer文にlambda関数を指定 */
defer factorial_l( 4 )
Rrk_print( "func1"\n )
Rrk_print( "func2"\n )
}
func()
ただし上の例でのfactorial_f、factorial_l は戻り値にこそ意味がある関数ですが、 defer文ではその戻り値を取得することはできません。 そのため、上の例でのこれらの関数はdefer文では実質的には意味を持ちません。 一応 factorial_f、factorial_l 関数内でRrk_printを入れておりますので、 この例の実行結果は以下のように表示されます。
func1
func2
factorial_l : i=4
factorial_l : i=3
factorial_l : i=2
factorial_l : i=1
factorial_f : i=4
factorial_f : i=3
factorial_f : i=2
factorial_f : i=1
実際に呼び出される位置を細かく調整したい場合
既に述べた通り、defer文で指定した関数は、実際にはdefer文が直属する関数が終了するタイミングで呼び出され実行されます。 そのため、(あまり頻繁にあることではないかもしれませんが)例えばdefer文をループ文の中で直に呼び出すのはほとんどの場合避けた方がよいでしょう。 例えば以下の例をご覧下さい。
import std/file
function example(){
for idx:=0; idx<10000; ++idx {
/* Danger! */
const fp = RrkFile_open( "test.txt", "rb" )
defer RrkFile_close( fp )
}
return /* 実際にはこの位置で RrkFile_close( fp ) がまとめて呼び出される */
}
example()
上記の例ではまずfor文の中で10000回RrkFile_openが呼び出されます。 一方、RrkFile_closeはfor文の各回の終了時ではなく関数exampleの終了時に10000回呼び出される形になります。 ただおそらくこの例では(OSにもよるでしょうが)途中でRrkFile_openができなくなり処理がブロックされるかあるいはクラッシュするでしょう。 このような場合、まずRrkFile_openとRrkFile_closeの呼応する範囲を一つの関数でラップし、そのラップした関数をループ文で繰り返す必要があります。 例えば以下の通りです。
import std/file
function example(){
function readTxt(){
const fp = RrkFile_open( "test.txt", "rb" )
defer RrkFile_close( fp )
/* この位置で RrkFile_close( fp ) が呼び出される */
}
for idx:=0; idx<10000; ++idx {
readTxt()
}
return
}
example()
上記の例ではまずRrkFile_openとRrkFile_closeをローカル関数readTxtにラップし、その上でそれをfor文で呼び出しています。 これによりRrkFile_closeはreadTxtの終了時に確実に呼び出され、狙い通りfor文の各回の終了時にRrkFile_closeされる形になります。
このように通常はローカル関数でラップする方法でも十分ですが、引数が多い場合などこれが面倒なこともあります。 defer文の実際の呼び出しを関数の終わりではなく例えば無名ブロックの終わりに呼び出すといったような調整はできないでしょうか? 残念ながらRarakuのdefer文は無名ブロックを認識しませんが、lambda式の即時呼び出しを使うことでほぼ同様のことが実現できます。 例えば以下の通りです。
function open(){
Rrk_print( "open" \n )
}
function close(){
Rrk_print( "close" \n )
}
function lock(){
Rrk_print( "lock" \n )
}
function unlock(){
Rrk_print( "unlock" \n )
}
/* 無名ブロックの替わりにlambda式の即時呼び出しを記述 */
^lam(){
open() defer close()
Rrk_print( "access1"\n )
/* 無名ブロックの替わりにlambda式の即時呼び出しを記述 */
^lam(){
lock() defer unlock()
Rrk_print( "critical1"\n )
Rrk_print( "critical2"\n )
/* unlockが呼び出されるのはこのタイミング */
}()
Rrk_print( "access2"\n )
/* closeが呼び出されるのはこのタイミング */
}()
Rrk_print( "after1"\n )
Rrk_print( "after2"\n )
上記でのlambda式の即時呼び出しは、実質的には無名ブロックとしてほぼ代用できます。
ただしその内部でreturn文やbreak文などを使った場合は、
もちろん無名ブロックの挙動と異なるものとなります。
またその内部におけるdefer文で指定されたclose関数の呼び出しは、このlambda式での最後で実際に呼び出されます。 unlock関数の呼び出しも同様です。
この例の実行結果は以下のようになります。
open
access1
lock
critical1
critical2
unlock
access2
close
after1
after2
defer文のまとめ
defer文には関数呼び出しを指定します。 これを用いてハンドラを生成する関数の呼び出しの直後に、解放する関数の呼び出しを一緒に書いておくことができます。 このとき解放する関数が実際に呼び出されるのは、一番最後になります。 ここで「一番最後」とは、defer文がある関数内で記述されていた場合はその関数が終了するタイミング、 グローバルスコープで記述されていた場合はそのファイルの一番最後になります。
ファイルハンドラを使う場合、RrkFile_open文の直後に defer文を置き、RrkFile_close を指定しておきましょう。 その方がRrkFile_closeの書き忘れを防止しやすいです。
defer文には無名関数やlambda式などの呼び出しを指定することもできます。 defer文で指定した関数の戻り値を直接取得することはできません。
目次に戻る
ヘッダファイルとリンケージ
プログラムを複数のファイルに分ける
プログラムの開発規模が大きくなると、すべての処理を一つのソースファイルに記述するのは管理が難しくなります。 Rarakuでは全体を複数のソースファイルに分割して記述することもできます。 このようにすれば管理もしやすくなりますが、このセクションではまずそのための準備となる事項を述べ、 その具体的な方法について説明します。
外部リンケージと内部リンケージ
「ブロックとスコープ」のセクションではスコープについて述べました。 このようなスコープは一つのファイルを超え、複数のファイルに跨るような場合もあります。
例えば「複数のファイルに跨る変数」といったものも考えられます。 リンカを持つ言語(Rarakuもそうですが)の場合、一般的にはこれを外部リンケージを持つ変数と呼びます。 あるいは混乱しない文脈では単にこれをグローバル変数と呼ぶこともあります(C言語で言えばexternグローバル変数です)。 逆に「ある一つのファイル内だけで限定して使えるような変数」のことを内部リンケージを持つ変数と呼びます。
Rarakuで変数を宣言した場合、それはすべて内部リンケージを持つ変数となります。
Rarakuでは外部リンケージを持つ変数を宣言することはできません。
リンケージ(linkage)という単語は「結合」「連携」といった意味ですが、
ここでは外部リンケージと内部リンケージを総称してリンケージと呼ぶことにします。
定数についても同様です。 すなわち外部リンケージを持つ定数や、内部リンケージを持つ定数を考えることができます。 例えば円周率のように汎用的に使われる定数やプログラムのバージョン番号などは、 外部リンケージを持つ定数として宣言するのもよいでしょう。
Rarakuでは、内部リンケージを持つ定数と外部リンケージを持つ定数のいずれの宣言も可能です。
ソースファイル(rrksファイル)内で宣言された定数は内部リンケージ、
ヘッダファイル(rrkhファイル)内で宣言された定数は外部リンケージを持ちます。
尚、細かいことを言えば、定数の場合はコンパイル時に直接コードへ埋め込まれるような処理がなされるため、 外部リンケージという表現はあまり適切ではないかもしれませんが、ここでは変数の場合と対比させるため、 このような表現を使うことにします。
尚、細かいことを言えば、定数の場合はコンパイル時に直接コードへ埋め込まれるような処理がなされるため、 外部リンケージという表現はあまり適切ではないかもしれませんが、ここでは変数の場合と対比させるため、 このような表現を使うことにします。
さらに外部リンケージを持つ関数、内部リンケージを持つ関数を考えることができます。 混乱しない文脈では、前者を単にグローバル関数、後者をスタティック関数と呼ぶこともあります。
これまで functionというキーワードを用いてグローバルスコープに関数を定義してきましたが、
それらはすべて内部リンケージを持つ関数になります。
外部リンケージを持つ関数として定義したい場合、キーワード「function」の替わりにキーワード「global」を使います。
これについてはすぐ後で詳しく述べます。
外部リンケージを持つ変数は、特に大規模なプログラムではその値の変遷を非常に追いにくいため、極力その使用を避けるべきです。 Rarakuではそもそもそのような変数の宣言自体ができませんが、ではそのような変数がどうしても必要な場合はどうすればよいでしょうか? Rarakuではグローバル関数の宣言は可能ですので、これを介して内部リンケージの変数へアクセスします。 これにより、間接的にグローバル変数への参照や、更新に相当する処理ができることになります。 このような関数をアクセサと呼びます。
参考: アクセサを使う意義
アクセサはset/getという名前が使われることが多いのでsetter/getterなどと呼ばれることもあります。
アクセサを使って同様のことができるならば結局何も変わらないと思われるかもしれませんが、 実際にはそれを使わずにグローバル変数自身を直に(様々なファイルの)コード内にばら撒いて記述する場合と比べれば、 まだ幾分問題が緩和されます。
アクセサ(setter)を用意し、値を更新する手段をそれに限定しておくことによって、 変数の値が更新される可能性があるのはそれを「関数呼び出し」している表記部分のみであることが保証されます。 このような保証があれば、そのアクセサの関数名でgrep等を使ってソースコード内を検索すれば、 (関数型変数を介するような捻くれたことをしていなければ)その変数が更新されている箇所を機械的に把握しやすくなります。 アクセサの名前を僅かに変え、再コンパイルするのもよいでしょう。 それが使われている箇所をコンパイラが「コンパイルエラー」として教えてくれます。 そして理想的にはその関数呼び出しを行う箇所を極少数のファイル、極少数の箇所に限定できれば尚よいでしょう。 そうなればほとんどの部分でこれを定数とみなせますからコードを把握するのがはるかに楽になります。
一方、アクセサを使わずにグローバル変数を直にコード内に記述した場合、左辺値か右辺値かを区別するため、 典型的には「変数名 = 値」という代入文のパターンをまず探す必要がありますが、これは機械的には見つけ辛いです。 そもそも、「=」ではない他の記法(「+=」など)で変数が更新される可能性もあり得ますし、 call-by-referenceな関数の引数として与えた場合などではもはや右辺値と字面上区別がつかないかもしれません。 そして変数の値が更新される可能性がある箇所を把握しにくいということは、結局どこかで更新されるかもしれない可能性を 常に考慮しなければならないことに繋がり、コードを把握するのがはるかに難しくなります。
アクセサのその他の効用としては、その中にassertを仕込んで置くことで値の変遷をより限定的な範囲に抑えるといったようなことができます。 あるいは内部で値を表示するような処理を仕込んで置けば値の変遷を簡単に可視化できます。
Close
アクセサを使って同様のことができるならば結局何も変わらないと思われるかもしれませんが、 実際にはそれを使わずにグローバル変数自身を直に(様々なファイルの)コード内にばら撒いて記述する場合と比べれば、 まだ幾分問題が緩和されます。
アクセサ(setter)を用意し、値を更新する手段をそれに限定しておくことによって、 変数の値が更新される可能性があるのはそれを「関数呼び出し」している表記部分のみであることが保証されます。 このような保証があれば、そのアクセサの関数名でgrep等を使ってソースコード内を検索すれば、 (関数型変数を介するような捻くれたことをしていなければ)その変数が更新されている箇所を機械的に把握しやすくなります。 アクセサの名前を僅かに変え、再コンパイルするのもよいでしょう。 それが使われている箇所をコンパイラが「コンパイルエラー」として教えてくれます。 そして理想的にはその関数呼び出しを行う箇所を極少数のファイル、極少数の箇所に限定できれば尚よいでしょう。 そうなればほとんどの部分でこれを定数とみなせますからコードを把握するのがはるかに楽になります。
一方、アクセサを使わずにグローバル変数を直にコード内に記述した場合、左辺値か右辺値かを区別するため、 典型的には「変数名 = 値」という代入文のパターンをまず探す必要がありますが、これは機械的には見つけ辛いです。 そもそも、「=」ではない他の記法(「+=」など)で変数が更新される可能性もあり得ますし、 call-by-referenceな関数の引数として与えた場合などではもはや右辺値と字面上区別がつかないかもしれません。 そして変数の値が更新される可能性がある箇所を把握しにくいということは、結局どこかで更新されるかもしれない可能性を 常に考慮しなければならないことに繋がり、コードを把握するのがはるかに難しくなります。
アクセサのその他の効用としては、その中にassertを仕込んで置くことで値の変遷をより限定的な範囲に抑えるといったようなことができます。 あるいは内部で値を表示するような処理を仕込んで置けば値の変遷を簡単に可視化できます。
Close
尚、変数ではなく定数であればそもそも値の変遷を追う作業自体が発生し得ないため、 スコープやリンケージの範囲を限定する意味は(名前空間の衝突の問題等を除けば)ほぼありません。 というより仮に内部リンケージを持つ定数だけに限定してしまうと多くの場合かえって問題になります。 同じ定数を複数の箇所に重複して記述してしまうことに繋がりかねないからです。
エントリーファイルとユーティライズファイル
では具体的に複数のソースファイルに分割して記述する例を見ます。
まず最も簡単な例としてプログラムのメインとなるソースファイルと、それ以外の下請け処理を行う関数を集めたソースファイルを作ります。 例えば前者をmain.rrks、後者をmy_util.rrksというファイル名にし、またそれらの内容は以下のようなものであるとしましょう (ただし実際にはこれだけではまだ不十分です)。
main.rrks
MyUtil_print( "hello" ) /* my_util.rrksで独自に定義された関数MyUtil_printを使いたい */
my_util.rrks
utilize
global MyUtil_print( str conststr ) void
{
/* MyUtil_printの具体的な実装(たった1行ですが) */
Rrk_print( str "\n" )
}
my_util.rrks ファイル内において、MyUtil_print関数を定義していますが、その定義文の開始はキーワード function ではなくキーワード global となっています。 これによりMyUtil_printを外部リンケージを持つ関数として定義できます。 すなわち main.rrks からこれを呼び出すことができるようになります(実際にはこれだけではまだ不十分ですがこれについてはすぐ後で述べます)。
Rarakuで複数のソースファイルを扱う場合、プログラムのエントリーとなるrrksファイルは一つでなければなりません。 このようなrrksファイルをRarakuではエントリーファイルと呼びます。 Rarakuのプログラムは、通常エントリーファイルにおけるグローバルスコープから実行が開始されます。 上の例ではmain.rrksがエントリーファイルです。
一方、プログラムのエントリーとはならず、単に他のソースファイルから呼び出される関数などの集まりからなるrrksファイルのことを Rarakuではユーティライズファイルと呼びます。 上の例ではmy_util.rrksがユーティライズファイルです。
ユーティライズファイル内には必ずキーワード「utilize」を記述しなければなりません。 実際、my_util.rrks内の一番上の行においても、このキーワード「utilize」が記述されています。 逆に言えば、このutilizeの指定がないrrksファイルはエントリーファイルとなります。
my_util.rrks内には外部リンケージを持つ関数を記述しますが、その名前の頭には「MyUtil_」というプレフィクスをつけるものとしましょう。 これは別に文法的に必須というわけではありませんが、関数名からどのソースファイル内で定義されたものかを一目でわかるようにしておけば、 それだけ管理もし易くなります(名前の衝突を防ぐという意味もあります)。
このように外部リンケージを持つ関数を定義する場合、その真の名前は極力冗長にすべきです。
入力が手間になり大変と感じるならば、テキストエディタの入力補完機能等を使いましょう。
例えばvimでは、入力モード中にCtrl+Pを押すと入力補完ができます。
尚、関数を使う場合際においては、「functype文とコールバック関数」のセクションで述べた関数のエイリアスを使って、 それに短い別名を付けた上で使うこともできます。
尚、関数を使う場合際においては、「functype文とコールバック関数」のセクションで述べた関数のエイリアスを使って、 それに短い別名を付けた上で使うこともできます。
ヘッダファイル
エントリーファイルとユーティライズファイルが用意できたならば、もう9割以上は完成しているのですが、 まだ最後の仕上げが必要です。
前項の例の場合ですと、このままではまだ main.rrks から my_util.rrks に定義されたMyUtil_printを呼び出すことができません。 MyUtil_printに関する窓口的な情報、すなわちそれがどのような引数を伴ってどのような戻り値を返す関数であるのかが、 このままではmain.rrks側からはわからず、コンパイルエラーとなるからです。 このような関数の窓口的な情報を関数のインターフェースと呼ぶこともあります。
RarakuのコンパイラはC言語のコンパイラと同様、実装ファイル単独で独立にコンパイルすることができます。
つまり他のrrksファイルの具体的な実装を不在にした状態であっても、
各rrksファイル単独で独立にコンパイル可能であるということです。
これはあるrrksファイルが依存するrrksファイル群の数が膨大である場合、再コンパイルにかかる時間を節約できるという観点からすれば望ましいと考えられます。
ただし、その場合でもそれらのrrks群の窓口的な情報だけは必要となり、
Rarakuではこれらを明示的に記述したヘッダファイルを別途用意するというスタイルをとっています。
main.rrks からMyUtil_printを呼び出すには、それがどのようなインターフェースであるのかをmain.rrks側に伝達する必要があります。
通常はユーティライズファイル X.rrks があった場合、それに対応するX.rrkhを一つ用意するのがよいでしょう。 今回の場合、例えば次のようにmy_util.rrks に対応するヘッダファイル my_util.rrkh を作成します。
my_util.rrkh
global MyUtil_print( str conststr ) void
上の例で「global MyUtil_print( str conststr ) void」までが一つの文です。 後ろに「{」と「}」で囲まれたブロックが続きませんから、これは関数宣言の一種となります。
これまで標準関数のヘッダファイル内で「natvfunc」で始まる関数宣言を見てきましたが、あれと同様のものです。
natvfuncで始まる場合、その本体となる実装はC言語によって記述されていることを示します。
一方、globalで始まる場合、その本体となる実装はRaraku言語によって何らかのrrksファイルに記述されていることを示します。
このとき戻り値の型がvoidである場合でも、そのvoidを省略することはできません。
functionで始まる場合や無名関数やlambda式などではvoidを省略できたのですが、
一方でnatvfuncやglobalで始まる場合はそれを省略できないということです。
このような関数宣言はその関数の実体の定義より前に記述しますので、これをプロトタイプ宣言と呼ぶこともあります。 尚、プロトタイプ宣言は必ずヘッダファイルに記述しなければなりません。 ヘッダファイル以外の場所でプロトタイプ宣言を記述した場合はコンパイルエラーとなります。 さらにこれは(グローバルブロックや関数内ではなく)グローバルスコープに記述する必要があります。
natvfuncやglobalで始まる関数においては、プロトタイプ宣言が必須であり、
例えばあるglobalな関数を仮に1つのファイルでのみ使うような場合も、それに対するプロトタイプ宣言が必須となります
(つまり1つのファイルでしか使わないような場合でも別途ヘッダファイルが必要になるということですが、
そもそも1つのファイルでしか使わないような関数ならば、最初からglobalキーワードではなくfunctionキーワードを指定すべきです)。
一方、functionで始まる関数、無名関数、lambda式などをプロトタイプ宣言する必要はありませんし、 これらに対するプロトタイプ宣言をした場合、逆にコンパイルエラーとなります。
一方、functionで始まる関数、無名関数、lambda式などをプロトタイプ宣言する必要はありませんし、 これらに対するプロトタイプ宣言をした場合、逆にコンパイルエラーとなります。
ヘッダファイルを一言で総括すれば、窓口となるようなファイルです(C言語におけるヘッダファイルと役割は同じです)。
尚、ヘッダファイル内では「utilize」キーワードの記述は不要です。
またRarakuでは一度インポートされたヘッダファイルは、重複して再度インポートされることはありません。 C言語のヘッダと異なり、いわゆる「多重インクルード防止マクロ」のような記述をヘッダ内に含める必要はありません。
またRarakuでは一度インポートされたヘッダファイルは、重複して再度インポートされることはありません。 C言語のヘッダと異なり、いわゆる「多重インクルード防止マクロ」のような記述をヘッダ内に含める必要はありません。
次にmy_util.rrksでは以下のようにmy_util.rrkhをimportしなければなりません。
my_util.rrks
utilize
import my_util /* required. */
global MyUtil_print( str conststr ) void
{
/* MyUtil_printの具体的な実装 */
Rrk_print( str "\n" )
}
my_util.rrks内に書かれたglobal関数に対応するプロトタイプ宣言は、(my_util.rrksから参照可能な位置に)どこかにただ一つ存在しなければなりません (対応するプロトタイプ宣言が存在しない場合や二つ以上宣言されていた場合はコンパイルエラーとなります)。 さらにそのプロトタイプ宣言はヘッダに書かなければならないというルールもありますから、 結局それを記述したmy_util.rrkhのimportが必須になるというわけです。
最後にmain.rrks側では以下のようにmy_utilをimportします。
main.rrks
import my_util /* my_util.rrkh をインポート */
MyUtil_print( "hello" ) /* my_util.rrksで独自に定義された関数MyUtil_printを使いたい */
これを記述する位置は、MyUtil_printの呼び出しより前のグローバルスコープであればmain.rrks 内のどこでも構いませんが、
通常はわかりやすくするためファイルの一番上の方に記述しましょう。
このimportがない場合、main.rrksからはMyUtil_printがどのような関数であるのかを認識することができないため、
やはりコンパイルエラーとなります。
上記のルールにより、my_util.rrkh内にあるプロトタイプ宣言(「global MyUtil_print( str conststr ) void」)は、
実装側と呼び出し側双方から共通に参照されることになります。
換言すれば、実装側と呼び出し側の関数の型はただ一つのプロトタイプ宣言を介して確かに一致することが保証されるわけです。
例えばmy_util.rrksに新しいグローバル関数MyUtil_loadなるものが追加されたとしましょう。
このとき、(MyUtil_loadのプロトタイプ宣言を)ヘッダファイルにも追加しなければなりません
(これを忘れた場合、何らかの形でコンパイルエラーとなりますのですぐに気付くことはできます)。
あるいは rrks 内で関数の引数の型や個数などを修正したとします。 この場合、rrkh の方の同関数のプロトタイプ宣言も同様に修正する必要が生じます (これを忘れた場合も、何らかの形でコンパイルエラーとなりますのですぐに気付くことはできます)。
ヘッダファイルを介することで、それをimportするすべてのファイルに対し、新たに追加された関数のインターフェスの情報を 自動的に伝達することができます。
あるいは rrks 内で関数の引数の型や個数などを修正したとします。 この場合、rrkh の方の同関数のプロトタイプ宣言も同様に修正する必要が生じます (これを忘れた場合も、何らかの形でコンパイルエラーとなりますのですぐに気付くことはできます)。
ヘッダファイルを介することで、それをimportするすべてのファイルに対し、新たに追加された関数のインターフェスの情報を 自動的に伝達することができます。
Coffee Break: 最後の「{」をカットする手間を省く
細かい話ですが、ユーティライズファイルにおける関数の定義(実装)では、
以下のようにブロックの開始となる「{」を戻り値の型の記述の次の行に書いた方がおそらく便利です。
つまり次のようには書かない方がおそらく便利と言うことです。
通常 rrksファイルに関数の定義を先に書いて、その最初の(関数名が書かれた)行をrrkhにコピペするようなことが多いと思います。 これを考慮した場合、前者のスタイルならば「global MyUtil_print( str conststr ) void」の1行をそのままコピペすれば終わります。 一方、後者のスタイルの場合、「global MyUtil_print( str conststr ) void {」の1行をコピペし、最後の「{」をカットする僅かな手間が加わります。
Close
global MyUtil_print( str conststr ) void
{
Rrk_print( str )
}
つまり次のようには書かない方がおそらく便利と言うことです。
global MyUtil_print( str conststr ) void {
Rrk_print( str )
}
通常 rrksファイルに関数の定義を先に書いて、その最初の(関数名が書かれた)行をrrkhにコピペするようなことが多いと思います。 これを考慮した場合、前者のスタイルならば「global MyUtil_print( str conststr ) void」の1行をそのままコピペすれば終わります。 一方、後者のスタイルの場合、「global MyUtil_print( str conststr ) void {」の1行をコピペし、最後の「{」をカットする僅かな手間が加わります。
といってもこのくらい全く大した手間ではないので、かなりどうでもいい話ではありますが…。
Close
参考: ヘッダファイルを使わない形式との比較
ヘッダファイル方式ではrrksと同じ関数を(実装部のない宣言とはいえ)rrkhにも書かなければなりません。
このように同じ記述を複数の箇所に重複して記述するのは、通常はあまり望ましいことではありません。
実際、最近の言語ではむしろヘッダファイル形式を採用していないものの方が多く、またヘッダがないためこのような冗長性はありません。 つまりそのような言語では二つのファイルに同じインターフェースを記述するといったようなことはなく、 一つの実装ファイルがインターフェースの公開の役割をも兼ねます。 今この方式を「非ヘッダファイル方式」と呼ぶことにしましょう。
非ヘッダファイル方式では、実装ファイルが巨大であったり複数のファイルに渡る場合、窓口にあたる情報を探し出すのが少々面倒になります。 それに加え、窓口にあたるglobal関数がただ一つであった場合にやや不恰好になります。 このとき、ヘッダファイル方式であればたった一行からなるファイル一つをプログラマに提示するだけで済みますが、 一方で、非ヘッダファイル方式では基本的に(globalに公開するシンボルを全く含まない完全にプライベートなファイルを除き)すべての実装ファイルを プログラマに提示する必要があるからです。
非ヘッダファイル方式でもドキュメントの自動生成機能がデフォルトで整っている言語であれば、 それを生成することで人間の目でわかりやすい一覧の形でインターフェースの情報を得ることができます。 生成されたドキュメントが「本当に完全」であればそれでもよいでしょう。
どうしても面倒であれば実装ファイルの内容を元にヘッダファイルを自動生成するツールを作っておくのもよいかもしれません。
ただ、Rarakuの場合、rrkhファイルにはグローバルな定数や簡易なインライン関数に相当するものを独自に記述することもできます。 その意味において、rrkh ファイルは一種のドキュメントとしての意味とソースコードとしての役割の双方を持ち合わせたものとなっています。
またRarakuの場合、インポート可能なのはrrkhファイルのみであり、rrksをインポートすることは許可されません。 全く同じことですが、逆に言えばrrksは他のどのファイルからもインポートされ得ないということになります。 このようにインポートされ得るファイルとされ得ないファイルを拡張子によって明確に区分しておく方がRarakuでは望ましいと考えました。 インポートされ得るファイルの内容は、(それをインポートする)他のファイルの処理に直接の影響を与えます。 そのためその記述はそれなりに慎重である必要がありますが、ファイル形式が明確に区分されてあれば (全部のファイルについてその種の注意を向ける必要なくなる分)労力を節約できる効果が期待できるためです。
Close
実際、最近の言語ではむしろヘッダファイル形式を採用していないものの方が多く、またヘッダがないためこのような冗長性はありません。 つまりそのような言語では二つのファイルに同じインターフェースを記述するといったようなことはなく、 一つの実装ファイルがインターフェースの公開の役割をも兼ねます。 今この方式を「非ヘッダファイル方式」と呼ぶことにしましょう。
非ヘッダファイル方式では、実装ファイルが巨大であったり複数のファイルに渡る場合、窓口にあたる情報を探し出すのが少々面倒になります。 それに加え、窓口にあたるglobal関数がただ一つであった場合にやや不恰好になります。 このとき、ヘッダファイル方式であればたった一行からなるファイル一つをプログラマに提示するだけで済みますが、 一方で、非ヘッダファイル方式では基本的に(globalに公開するシンボルを全く含まない完全にプライベートなファイルを除き)すべての実装ファイルを プログラマに提示する必要があるからです。
具体的な実装を隠蔽(使う側からはアクセス不可に)し、使う側にとって必要最低限のインターフェースだけが使用可能になっている
状態を情報隠蔽と呼びますが、ここではそれとは少し違う視点で話をしています。
つまり実装詳細への(コンパイラにとっての)アクセスの可否ではなく、
一連のコードをドキュメント的に捉えた場合の(人間にとっての)見通しのよさという視点から話をしています。
尚、Rarakuの情報隠蔽については「モジュールと情報隠蔽」のセクションで詳しく説明します。
尚、Rarakuの情報隠蔽については「モジュールと情報隠蔽」のセクションで詳しく説明します。
非ヘッダファイル方式でもドキュメントの自動生成機能がデフォルトで整っている言語であれば、 それを生成することで人間の目でわかりやすい一覧の形でインターフェースの情報を得ることができます。 生成されたドキュメントが「本当に完全」であればそれでもよいでしょう。
では(極端な仮定ですが)もし仮に定義を記述するrrksファイルの側に、インターフェースにあたる情報(つまり関数の引数や戻り値の型の情報)を
一切記述不要な仕様にしてしまえば、この種の重複は避けられるのではないでしょうか?
しかし、仮にそうすると実際に関数の実装をコーディングする作業において、
その関数の引数や戻り値の情報を確認するためにヘッダファイルに視点を移す必要があり、
さすがにコーディングし辛いと思われます。
結果的にこのインターフェース部分の重複については妥協するといった形に落ち着きます。
外部関数の呼び出しのために(その関数の仕様を説明した)ドキュメントや(その関数が宣言された)ヘッダファイルを確認するのは
仕方がないとしても、今実装している(自分自身の)関数の引数や戻り値の情報はさすがに瞬間的な視線の移動か
単なるエディタのスクロールで確認できた方がよいということです。
結果的にこのインターフェース部分の重複については妥協するといった形に落ち着きます。
どうしても面倒であれば実装ファイルの内容を元にヘッダファイルを自動生成するツールを作っておくのもよいかもしれません。
非ヘッダファイル形式であっても、内部的には(ヘッダファイルに相当するような)特殊な情報を一旦自動生成して処理するようにしているかもしれません。
例えば全体で使われるglobalシンボルの妥当性を検証するには、どのみちそのような情報を一覧として作成しなければならないと考えられます。
その観点からすれば、ヘッダファイル形式と非ヘッダファイル形式の境界はあいまいで、明確に区分することにあまり意味はないのかもしれません。
ただ、Rarakuの場合、rrkhファイルにはグローバルな定数や簡易なインライン関数に相当するものを独自に記述することもできます。 その意味において、rrkh ファイルは一種のドキュメントとしての意味とソースコードとしての役割の双方を持ち合わせたものとなっています。
そのため、ソースコードとしての役割があまりに大きく煩雑になると、
結局非ヘッダファイル形式の様相に近づくことにはなります。
またRarakuの場合、インポート可能なのはrrkhファイルのみであり、rrksをインポートすることは許可されません。 全く同じことですが、逆に言えばrrksは他のどのファイルからもインポートされ得ないということになります。 このようにインポートされ得るファイルとされ得ないファイルを拡張子によって明確に区分しておく方がRarakuでは望ましいと考えました。 インポートされ得るファイルの内容は、(それをインポートする)他のファイルの処理に直接の影響を与えます。 そのためその記述はそれなりに慎重である必要がありますが、ファイル形式が明確に区分されてあれば (全部のファイルについてその種の注意を向ける必要なくなる分)労力を節約できる効果が期待できるためです。
Close
複数のファイルをコマンドライン実行する
これで準備は整いました。 改めて今回作成した全てのファイルとその中身を以下に再掲します。 とりあえず今回の例ではこれで完成形となります。
my_util.rrkh
global MyUtil_print( str conststr ) void
my_util.rrks
utilize
import my_util
global MyUtil_print( str conststr ) void
{
/* MyUtil_printの具体的な実装(たった1行ですが) */
Rrk_print( str "\n" )
}
main.rrks
import my_util /* my_util.rrkh をインポート */
MyUtil_print( "hello" ) /* my_util.rrksで独自に定義された関数MyUtil_printを使いたい */
今回はrrksファイルが2つと独自に作成したヘッダが存在するため、rarakuコマンドの実行方法は少し変わり、以下のようになります。
raraku -pkg_path . main.rrks my_util.rrks
このように rrks ファイルが複数ある場合はそれらをコマンドライン引数にすべて指定します。 またインポートするヘッダが存在するパスを -pkg_path オプションの直後に指定します。 今回の場合カレントディレクトリに my_util.rrkh があるため、パスは「.」となります。
rrksファイルが多くなってくると毎回それらを全て指定するのも面倒になってくるかと思います。
その場合すべてのrrksファイルを一つの rrkx ファイル(これはバイナリファイルです)にまとめることもできます。
一度 rrkx ファイルを作れば以降の実行は rrkx ファイルを一つ指定するだけで済みます。
ただし rrkx ファイルの作成方法は若干複雑です。
まずすべての rrks ファイルに対応したRarakuオブジェクトファイル rrko を生成する必要があります。 この作業をコンパイルと呼びます(これまでの説明でもコンパイルという言葉を割とあいまいに使ってきましたが、 Rarakuにおけるコンパイルとは厳密に言えば rrks ファイルから rrko ファイルを生成することを意味します)。
rrko ファイルを生成するには raraku コマンドにおいて -compile オプションを指定し以下のように実行します。
上記により、デフォルトではカレントディレクトリにrrk_out_dirというディレクトリが自動的に生成され、 その rrk_out_dir の中に main.rrko、my_util.rrko というファイルが出来上がります。
次に生成した rrko ファイルをすべて集めて一つの rrkx ファイルにまとめます。 この作業をリンクと呼びます。 rrkx ファイルを生成するには raraku コマンドにおいて -link オプションを指定し以下のように実行します。
-linkオプション指定時では、一番最初に生成するrrkxファイルの名前(今回の場合main.rrkx)を指定し、 その後ろにその材料となるrrkoファイルを並べて指定します。 これにより、カレントディレクトリにmain.rrkx が生成されます。
生成した rrkx ファイルを実行するには単に以下のようにmain.rrkxファイルを指定して実行します。
rrks ファイルに修正が発生した場合はその都度上の手順を踏んで新しい rrkx ファイルを作り直さなければなりません。 ですから頻繁に修正作業が発生する開発段階では、rrks を直接実行する方式をとるか、Makefile等を作成しておく方が便利でしょう。
ただし rrkx ファイルの作成方法は若干複雑です。
まずすべての rrks ファイルに対応したRarakuオブジェクトファイル rrko を生成する必要があります。 この作業をコンパイルと呼びます(これまでの説明でもコンパイルという言葉を割とあいまいに使ってきましたが、 Rarakuにおけるコンパイルとは厳密に言えば rrks ファイルから rrko ファイルを生成することを意味します)。
rrko ファイルを生成するには raraku コマンドにおいて -compile オプションを指定し以下のように実行します。
raraku -pkg_path . -compile main.rrks
raraku -pkg_path . -compile my_util.rrks
上記により、デフォルトではカレントディレクトリにrrk_out_dirというディレクトリが自動的に生成され、 その rrk_out_dir の中に main.rrko、my_util.rrko というファイルが出来上がります。
次に生成した rrko ファイルをすべて集めて一つの rrkx ファイルにまとめます。 この作業をリンクと呼びます。 rrkx ファイルを生成するには raraku コマンドにおいて -link オプションを指定し以下のように実行します。
raraku -link main.rrkx rrk_out_dir/main.rrko rrk_out_dir/my_util.rrko
-linkオプション指定時では、一番最初に生成するrrkxファイルの名前(今回の場合main.rrkx)を指定し、 その後ろにその材料となるrrkoファイルを並べて指定します。 これにより、カレントディレクトリにmain.rrkx が生成されます。
生成した rrkx ファイルを実行するには単に以下のようにmain.rrkxファイルを指定して実行します。
raraku main.rrkx
このように raraku コマンドはコンパイラ、リンカ、インタプリタの役割を一つのコマンドで備えています。
rrks ファイルに修正が発生した場合はその都度上の手順を踏んで新しい rrkx ファイルを作り直さなければなりません。 ですから頻繁に修正作業が発生する開発段階では、rrks を直接実行する方式をとるか、Makefile等を作成しておく方が便利でしょう。
グローバルスコープで関数呼び出しができるのはエントリーファイルのみ
既に述べたように、キーワードutilizeをつけたファイルをユーティライズファイルと呼び、 それ以外の残りの一つをエントリーファイルと呼びます。 エントリーファイルは一つのプログラムにおいて一つだけ存在し、プログラムの入口となります。 ライブラリのようにエントリーファイルが存在しないものもありますが、 いずれにせよ最終的に出来上がる一つのプログラムにおいてエントリーファイルが二つ以上あってはいけません。
さて、これまでRarakuでは当然のように(関数外の)グローバルスコープで関数を呼び出してきましたが、 厳密に言えばこれはエントリーファイル内でのみ可能な記述となります。 一方、ユーティライズファイル内では、グローバルスコープで関数を呼び出すことは禁止されています。
ただしネイティブ関数の呼び出しであればユーティライズファイル内のグローバルスコープでも呼び出しは可能です。 また勿論、グローバルスコープではなく関数内であれば、ユーティライズファイル内でもこのような制限は一切ありません。
参考: このような仕様になっている理由
このような仕様になっている理由は、仮にこれを許可すると初期化の競合が生じる恐れがあるからです。
例えば今 A と B と C の3つのファイルがあり、この順でグローバルスコープを実行して初期化するとします。 仮に A のグローバルスコープからでも関数呼び出しが可能である場合、 A のグローバルスコープ実行時に(なんらかのルートを経由して) B の関数を呼び出してしまうと、 B のグローバルスコープはまだ未実行であるため、(B内の)未初期化な変数へアクセスする可能性を否定できません。 この可能性は排除しなければなりません。 従って、(A のグローバルスコープを先に実行するならば) A 内のグローバルスコープでの関数の呼び出しを封じざるを得ないわけです。
次に B のグローバルスコープの実行に移ったとします。 仮に B のグローバルスコープからでも関数呼び出しが可能である場合、既に初期化済みのAの関数を呼び出すだけならば一見問題なさそうですが、 実際には問題があります。Aの関数が(まだ未初期化な)Cの関数を呼び出しているかもしれないからです。 このときC内のグローバルスコープはまだ実行されていませんから、結果的に(C内の)未初期化な変数へアクセスする可能性を否定できません。 この可能性は排除しなければなりません。 従って B のグローバルスコープを実行するならば、これもまた B 内のグローバルスコープでのすべての関数の呼び出しを封じざるを得ないわけです。
最後に C のグローバルスコープの実行に移ります。 ここまで来ると、もはや C のグローバルスコープから関数呼び出しをしても問題ありません。 Cが一番最後ということは、それ以外のファイルはすべて初期化済みだからです(C内に一部未初期化部分が残っている可能性がありますが、 Rarakuではそれはまた別の機構でガードされます)。
Rarakuではエントリーファイルが一番最後に初期化され、まさに上記のCに該当します。 一方ユーティライズファイルはエントリーファイルよりも先に初期化され、これがまさに上記のA、Bに相当します。 ちなみにエントリーファイルが一つもない場合はユーティライズファイル群の初期化だけで全てが終了となります。
Close
例えば今 A と B と C の3つのファイルがあり、この順でグローバルスコープを実行して初期化するとします。 仮に A のグローバルスコープからでも関数呼び出しが可能である場合、 A のグローバルスコープ実行時に(なんらかのルートを経由して) B の関数を呼び出してしまうと、 B のグローバルスコープはまだ未実行であるため、(B内の)未初期化な変数へアクセスする可能性を否定できません。 この可能性は排除しなければなりません。 従って、(A のグローバルスコープを先に実行するならば) A 内のグローバルスコープでの関数の呼び出しを封じざるを得ないわけです。
次に B のグローバルスコープの実行に移ったとします。 仮に B のグローバルスコープからでも関数呼び出しが可能である場合、既に初期化済みのAの関数を呼び出すだけならば一見問題なさそうですが、 実際には問題があります。Aの関数が(まだ未初期化な)Cの関数を呼び出しているかもしれないからです。 このときC内のグローバルスコープはまだ実行されていませんから、結果的に(C内の)未初期化な変数へアクセスする可能性を否定できません。 この可能性は排除しなければなりません。 従って B のグローバルスコープを実行するならば、これもまた B 内のグローバルスコープでのすべての関数の呼び出しを封じざるを得ないわけです。
最後に C のグローバルスコープの実行に移ります。 ここまで来ると、もはや C のグローバルスコープから関数呼び出しをしても問題ありません。 Cが一番最後ということは、それ以外のファイルはすべて初期化済みだからです(C内に一部未初期化部分が残っている可能性がありますが、 Rarakuではそれはまた別の機構でガードされます)。
Rarakuではエントリーファイルが一番最後に初期化され、まさに上記のCに該当します。 一方ユーティライズファイルはエントリーファイルよりも先に初期化され、これがまさに上記のA、Bに相当します。 ちなみにエントリーファイルが一つもない場合はユーティライズファイル群の初期化だけで全てが終了となります。
Close
リンク時にエントリーファイル指定されたファイルが二つ以上あってはいけません。 つまりリンク時のエントリーファイルは0個か1個でなければなりません。 ユーティライズファイルは何個でも構いません。
内部リンケージを持つ関数
Rarakuではキーワードglobal以外で関数を定義した場合、それらはすべて内部リンケージを持つ関数となります。 他のファイルから使われたくない関数を作成したいならば、単にキーワードglobalの使用を避けるだけでOKです (ただしnatvfuncキーワードで宣言された関数は、外部リンケージ扱いとなります)。
C言語でそのような内部リンケージを持つ関数を定義するにはキーワード「static」を付けなければなりませんが、
Rarakuではそのような指定は不要です。
内部リンケージを持つ関数であっても、それを一旦関数型変数に代入して他のファイルへ渡すことで、
間接的に他のファイルで実行できてしまう場合もあります。
ただそのようなことを行う場合は必ず仲介役となるグローバル関数を別途用意する必要があります。
そのような特殊なグローバル関数を明示的に提供しない限りは、他のファイルからこれにアクセスする方法はありません。
Rarakuで内部リンケージを持つ関数についてまとめますと、次のようになります。
- スタティック関数
- ローカル関数
- lambda関数
- 無名関数
- lambda式
グローバルスコープにおいて、キーワード「function」により関数を定義した場合、 今まではそれを「通常の関数」と呼んできましたが、 これは正確にはスタティック関数(static function)と呼びます (関数内において、キーワード「static」によりスタティック関数を定義することはできません)。
スタティック関数は基本的にそのファイル内のどこからでも自由に(その名前による)直接呼び出しをすることができます。 ただし正確には完全に自由ではなく、スタティック関数内で外部の変数/定数にアクセスしている場合で、 しかもその呼び出しによって結果的に危険な状況が発生し得ると判断される場合、Rarakuはこれをコンパイルエラーとします。 それがどういう状況かについてはすぐ後で詳しく述べます。
無名ブロック内や関数内において、キーワード「function」により関数を定義した場合、 それをローカル関数と呼ぶのでした。
ローカル関数はそれが定義されたブロック内かつその定義より順序が後のコードからのみ(その名前による)直接呼び出しをすることができます。 ただし一旦関数型変数に格納し、その変数の値を別のブロック内まで持って行った上でそこから間接的に呼び出すといったことは可能です。
ローカル関数では、その内部からその外側にある(ローカル関数/無名ブロック内の)ローカル変数を参照することはできません。
無名ブロック内や関数内において、キーワード「lambda」により関数を定義した場合、 それをlambda関数と呼ぶのでした (グローバルスコープにおいて、キーワード「lambda」により関数を定義することはできません)。
lambda関数はそれが定義されたブロック内かつその定義より順序が後のコードからのみ(その名前による)直接呼び出しをすることができます。 ただし一旦関数型変数に格納し、その変数の値を別のブロック内まで持って行った上でそこから間接的に呼び出すといったことは可能です。
lambda関数では、その内部からその外側にある(ローカル関数/無名ブロック内の)ローカル変数を参照することができます。
lambda式/lambda関数からその外側のローカル変数を探索する場合、ローカル関数が登場した時点でその探索は打ち切られます。
例えばlambda関数の外側に二つ以上ローカル関数がネストされている場合は注意が必要です。
この場合(lambda関数から外側へ向かって見たときの)二つ目以降のローカル関数の変数を参照することはできません。
このようなローカル変数を参照したい場合はネストの途中もlambda関数にする必要があります。
これについてはすぐ後の「何重にもネストされたローカル関数とlambda関数/lambda式」の項目で詳しく述べます。
基本的にファイル内の任意の場所において、演算子「^fun」により関数を式として定義した場合、それを無名関数と呼ぶのでした (これは厳密には関数というより式ですが、慣用的に「無名関数」と呼ばれるためこの記事でもその呼称を使い、 ここでは一応内部リンケージを持つ関数として分類しています)。
無名関数は名前がないため、名前による直接呼び出しはそもそもできません。 基本的には一旦関数型変数に格納してからそれを間接的に呼び出して使いますが、 定義の位置で即時に呼び出して使うことも一応可能です。
基本的にファイル内の任意の場所において、演算子「^lam」により関数を式として定義した場合、それをlambda式と呼ぶのでした (これは厳密には関数というより式ですが、 ここでは一応内部リンケージを持つ関数として分類しています)。
lambda式は名前がないため、名前による直接呼び出しはそもそもできません。 基本的には一旦関数型変数に格納してからそれを間接的に呼び出して使いますが、 定義の位置で即時に呼び出して使うことも一応可能です。
インターナルスタティック関数
スタティック関数は、グローバルブロック内において、キーワード「function」の替わりにキーワード「static」を使うことで定義することもできます (ブロックの中での定義ですが、キーワード「function」ではないため、これはローカル関数とはなりません)。 このスタティック関数をRarakuでは特にインターナルスタティック関数(internal static function)と呼びます。
グローバルスコープにおいては、キーワード「function」と「static」のどちらのキーワードを使っても全く同じ効果となり、 すべてスタティック関数(通常の関数)として定義されます。 尚、インターナルスタティック関数と対比する文脈ではこのスタティック関数を特にエクスターナルスタティック関数(external static function)と呼んで、 インターナルスタティック関数と区別するものとします。
キーワード「static」と「function」を続けて「static function」と表記するようなことはできません。
例えば以下の通りです。
static_func() /* OK: Calling internal static function( from out of the global block ) */
{
/* In global blcok */
static_func() /* OK: Calling internal static function */
static static_func(){ /* Definition of internal static function */
Rrk_print( "hello static func in block\n" )
}
static_func() /* OK: Calling internal static function */
}
static_func() /* OK: Calling internal static function( from out of the global block ) */
上記でstatic_funcはインターナルスタティック関数となります。 一般にブロック内にあるものはブロック外からアクセスすることはできませんが、 インターナルスタティック関数は特例として、ブロック外からでもこれを(名前により)直接呼び出すことができます。
一方、インターナルスタティック関数を(グローバルブロック内ではなく)関数内で定義することはできません。 例えば以下はコンパイルエラーとなります。
function my_func()
{
/* In function block */
static static_func(){ /* Compile error : we want to define this as internal static function, but cannot in function block */
Rrk_print( "hello static func in function\n" )
}
}
また関数内でなくとも、無名ブロック以外のブロックに含まれているような場合は、 やはりその中でスタティック関数を定義することはできません。 例えば以下はコンパイルエラーとなります。
if( false ){
/* グローバルなブロックではあるが、無名ブロックではなくif文のブロック */
static static_func(){ /* コンパイルエラー : スタティック関数として定義したいが、ifブロック内では許可されない */
Rrk_print( "hello static func in if-block\n" )
}
}
何重ものブロックに囲まれている場合、それらのブロックがすべて無名グローバルブロックであるなら問題ありません。 例えば以下はOKです。
{
{
{
static static_func(){ /* OK : すべて無名グローバルブロック */
Rrk_print( "hello static func in function\n" )
}
}
}
}
一方、何重ものブロックに囲まれており、その途中で(if文のブロック等)無名ブロック以外のブロックが現れる場合、 その中でスタティック関数を定義することはできません。 例えば以下はコンパイルエラーとなります。
{
if( false ){
{
static static_func(){ /* コンパイルエラー : 途中にifブロックがある */
Rrk_print( "hello static func in if-block\n" )
}
}
}
}
上記に加え、goto文とラベル文の間に挟まれているようなグローバルブロック内のスタティック関数は定義できません。
goto文とラベル文については「goto文とラベル文」のセクションで詳しく述べます。
スタティック関数内から外部の変数へアクセスしている場合、場合によってはその呼び出しが許可されない場合があります。 例えば以下の通りです。
/* A */
static_func() /* Compile error : 危険なスタティック関数の呼び出し */
{
/* B */
static varia st_ival = 10
/* C */
static static_func(){ /* スタティック関数の定義 */
Rrk_print( \=st_ival\n )
}
/* D */
}
/* E */
上記では static_func関数内からその外部にあるstatic変数st_ivalにアクセスしています。 ここで注意して頂きたいのはこのst_ivalを宣言しているvaria文が実行されるタイミングです。 コード内の /* A */ や /* B */ の位置にいるタイミングでは、このvaria文はまだ実行されていません。 つまりこの時、変数st_ivalはまだ未初期化(上記では10という初期値が代入されるより前の段階)です。
C++などでは全てのプログラムの実行に先立ってまずstatic変数(クラス)の初期化だけが事前に行われます
(meyers-singletonと呼ばれる手法を用いるなど特別な場合を除きます)。
このようにすれば、プログラムが実行される段に置いてはすべてのstatic変数が初期化されていることが保証されますので、
上記の例のようなアクセスが問題なく行えることになります。
しかしRarakuではそのような初期化の方式は採用していません。 そのような特別な順番で初期化を行いますとその順序の把握が困難になり、特にデータ同士が複雑に依存関係を持つ場合、 その初期化処理の(順序の)妥当性を検証するのが難しくなります。 また、いざその部分でバグを入れてしまいますとその原因の特定もまた著しく困難になり、別の意味で危険と考えられるためです。
一方C言語ではstatic変数の右辺に指定できる値は数値リテラルや文字列リテラル等に限られ、 その初期化はコンパイル時に静的に処理されます。 このような単純な値の初期化であればそれが処理される順番は問題になりませんし、 そもそもバグの入りようもありませんので上述のC++のような問題は起こり得ませんが、 やはり柔軟性はそれだけ損なわれます。 Rarakuではstatic変数の右辺に動的な関数値を指定することもできますので、 C言語のような静的な方式も(コンパイル時に明らかに最適化できる場合を除き)また採用していないというわけです。
しかしRarakuではそのような初期化の方式は採用していません。 そのような特別な順番で初期化を行いますとその順序の把握が困難になり、特にデータ同士が複雑に依存関係を持つ場合、 その初期化処理の(順序の)妥当性を検証するのが難しくなります。 また、いざその部分でバグを入れてしまいますとその原因の特定もまた著しく困難になり、別の意味で危険と考えられるためです。
一方C言語ではstatic変数の右辺に指定できる値は数値リテラルや文字列リテラル等に限られ、 その初期化はコンパイル時に静的に処理されます。 このような単純な値の初期化であればそれが処理される順番は問題になりませんし、 そもそもバグの入りようもありませんので上述のC++のような問題は起こり得ませんが、 やはり柔軟性はそれだけ損なわれます。 Rarakuではstatic変数の右辺に動的な関数値を指定することもできますので、 C言語のような静的な方式も(コンパイル時に明らかに最適化できる場合を除き)また採用していないというわけです。
ところが上記では /* A */ の位置でstatic_funcを呼び出しています。 仮にこれを許可した場合、varia文の実行をスキップしていきなりstatic_func関数内に入れてしまい、 その中でまだ未初期化であるst_ivalにアクセスするようなことが起こってしまいます。
Rarakuはグローバルスコープを上から順に実行し、
(エントリーファイルに限られますが)そこで関数呼び出しさえも出来る言語です。
この問題は要するに、グローバルスコープでまだ未初期化な変数がある状態でそのような関数呼び出しができてしまうことにより起こります。
ですから、そのような呼び出しを認めていない言語(C言語やGo言語などがそうですが)ではそもそもこの問題は発生しないことになります。
通常、未初期化状態の変数にアクセスすることはバグの元となりますので許可すべきではありません。 そのため、Rarakuでは上記のようなタイミングでstatic_funcを呼び出した場合、これをコンパイルエラーとします。 これは /* B */ の位置で呼び出した場合も同様です。
一方、/* C */ や /* D */ や /* E */ の位置では既にこのvaria文は実行済みです。 つまりこの位置からならstatic_funcを呼び出しても問題ありませんから、これらの位置からの呼び出しは許可されます。
インターナルスタティック関数の宣言であるブロック内のみで有効な構造体などを使用しており、 さらにそのブロック外からそのインターナルスタティック関数を呼び出した結果、 型推論などによってその構造体型の変数が必然的に宣言されるような状況になるとき、 コンパイルエラーとなります。 例えば以下の通りです。
{
struct InternalMyStruct {
m int
}
static static_func() InternalMyStruct { /* internal static function with InternalMyStruct */
varia s InternalMyStruct
return s
}
varia u = static_func() /* OK */
}
varia v = static_func() /* Compile error : v is InternalMyStruct !? */
仮に上記を許可してしまうと、変数 v の型をInternalMyStructと型推論する必要がありますが、 一方でInternalMyStructはブロック外につきもう失効しているため、ここで不整合が生じてしまいます。
関数名の衝突とシャドウイング
関数を定義する場合、基本的には他の関数と名前が重複しないようにした方がよいでしょう。 しかしここでは敢えて関数名を重複させた場合どうなるかを見ます。
Rarakuでスタティック関数を定義する場合、それと他の関数との名前が重複するような状況は基本的に許可されません。 具体的にはスタティック関数を定義しようとしているのと同じブロック内、あるいはそれより親となるブロック内に、 (そのスタティック関数と)同じ名前の関数定義や関数宣言がある場合、コンパイルエラーとなります (ただしその相手がfunctype文やローカル関数の場合は許可されます)。 例えば以下の通りです。
function func() void { /* スタティック関数の定義 */
Rrk_print( "static func 1\n" )
}
{
/***
* Compile error : 既にfuncという名前のスタティック関数が定義されているブロックの子ブロックに、
* 同名のスタティック関数を定義しようとしている.
*/
static func(){ /* スタティック関数の定義 */
Rrk_print( "static func 2\n" )
}
}
上記では、既にスタティック関数funcがグローバルスコープに定義されており、 しかもそれを参照可能な子ブロック内に同名のスタティック関数funcを定義しようとしているため、コンパイルエラーとなります。
natvfunc func() void; /* prototype declaration */
{
/***
* Compile error : 既にfuncという名前でネイティブ関数の関数宣言がされている状態で、
* 同名のスタティック関数を定義しようとしている.
*/
static func(){ /* スタティック関数の定義 */
Rrk_print( "static func\n" )
}
}
上記では、既にfuncという名前でネイティブ関数の関数宣言がされており、 しかもそれが参照可能な子ブロック内に同名のスタティック関数funcを定義しようとしているため、コンパイルエラーとなります。 尚、これがネイティブ関数ではなくグローバル関数であっても同様です。
C言語ではグローバル関数を同名のスタティック関数でシャドウイングすることができ、
この機能は例えばデバッグなどで利用することができます。
既に説明したようにRarakuではスタティック関数で直接シャドウイングをすることはできませんが、 似たようなことを実現したい場合は以下のように一旦同名の関数型変数にスタティック関数func_debugを代入した上で呼び出すとよいでしょう。
あるいはfunc_debugがスタティック関数である必要がない場合は、変数funcの右辺として無名関数を用いてもよいでしょう。 Rarakuでは関数型変数とスタティック関数の組み合わせであれば、それがたとえ同一ブロック内であっても名前を重複させても構わない規則になっています。 その場合どちらが優先して呼ばれるかは、最後の例で詳しく説明します。
既に説明したようにRarakuではスタティック関数で直接シャドウイングをすることはできませんが、 似たようなことを実現したい場合は以下のように一旦同名の関数型変数にスタティック関数func_debugを代入した上で呼び出すとよいでしょう。
function func( i int, j int ) void {
return i + j
}
{
static func_debug( i int, j int ){
Rrk_print( \=i \, \=j \n )
func( i, j )
}
}
const func = func_debug /* Assign to the variable 'func' of the same name */
/* We do not want to modify function names in the code that follows. */
const x = func( 2, 3 )
varia y = func( x, -1 )
あるいはfunc_debugがスタティック関数である必要がない場合は、変数funcの右辺として無名関数を用いてもよいでしょう。 Rarakuでは関数型変数とスタティック関数の組み合わせであれば、それがたとえ同一ブロック内であっても名前を重複させても構わない規則になっています。 その場合どちらが優先して呼ばれるかは、最後の例で詳しく説明します。
次の例は同名のスタティック関数との組み合わせが許可される例です。
functype func() void
{
/***
* OK : 既にfuncという名前でfunctype文が定義されているが、この場合は同名のスタティック関数を定義してもよい.
*/
static func(){ /* スタティック関数の定義 */
Rrk_print( "static func\n" )
}
}
上記では、既にfunctype文がfuncという名前でグローバルスコープに定義されていますが、 この場合は、同名のスタティック関数funcを定義しても構いません。
function func() void { /* スタティック関数 */
Rrk_print( "static func\n" )
}
{
/***
* OK : 既にfuncという名前の関数が定義されているが、同名のローカル関数ならば定義してもよい.
*/
function func(){ /* ローカル関数の定義 */
Rrk_print( "local func\n" )
}
func() /* A : ローカル関数の方のfuncが呼び出される */
}
func() /* B : スタティック関数の方のfuncが呼び出される */
上記では、既にスタティック関数funcがグローバルスコープに定義されていますが、 それを参照可能な子ブロック内に同名のローカル関数funcを定義しています。 後に来ている子ブロック内で定義しているのは(スタティック関数ではなく)ローカル関数であるため、これは許可されます。 このときローカル関数funcが定義されているブロック内ではその名前がシャドウイングされ、 Aの位置での関数呼び出し func() ではローカル関数の方のfuncが呼び出され、 Bの位置での関数呼び出し func() ではスタティック関数の方のfuncが呼び出されます。 この例の実行結果は以下のようになります。
locak func
static func
次の例の挙動は少し予想に反するかもしれません。
{
function func() void {
Rrk_print( "locak func\n" )
}
{
/***
* OK : 既にfuncという名前でローカル関数が定義されているが、この場合は同名のスタティック関数を定義してもよい.
*/
static func(){ /* スタティック関数の定義 */
Rrk_print( "static func\n" )
}
func() /* A : ローカル関数の方のfuncが呼び出される */
}
func() /* B : ローカル関数の方のfuncが呼び出される */
}
func() /* C : スタティック関数の方のfuncが呼び出される */
上記では、既にローカル関数funcがグローバルスコープに定義されていますが、 それを参照可能な子ブロック内に同名のスタティック関数funcを定義しようとしています。 先に定義されているのはローカル関数であるため、これは許可されます。
このとき位置A, B, Cにおける関数呼び出し func() で実際に呼び出される関数はどれでしょうか? 位置Cにおいてはスタティック関数のfuncで呼び出されることは明らかでしょう(この位置から参照可能な関数はスタティック関数のfuncしかないためです)。 しかし位置A, Bにおいては、ローカル関数とスタティック関数の両方が参照可能です。
スタティック関数はその定義の外のブロックからも呼び出せるという特殊性のため、その呼び出しの際、変数の参照とは独立して探索されます。 関数の呼び出しではまず関数型変数の探索が先に行われ、その後にスタティック関数やグローバル関数などのテーブルから探索が行われます。 さらにローカル関数の定義は関数型変数に無名関数を代入したもののシンタックスシュガーであることを思い出しましょう。 従って、位置A, Bにおいて探索で先に見つかるのはローカル関数(実体は変数)の方であり、こちらが実行されることになります。 この例の実行結果は以下のようになります。
locak func
locak func
static func
何重にもネストされたローカル関数とlambda関数/lambda式
まず次のように何重にもネストされたlambda関数を考えます(あまり考えたくないかもしれませんが)。
/* Global Scope */
varia st_global_var int/* OK */
{ /* Global Block */
varia gblk_var int/* OK */
lambda lamb1(){
varia local1_var int/* OK */
lambda lamb2(){
varia local2_var int/* OK */
lambda lamb3(){
/***
* Referable from here:
*/
local2_var = 9
local1_var = 9
gblk_var = 9
st_global_var = 9
}
}
}
}
上記の例では、一番最内のlambda関数lamb3から出発し、その外側向かって探索したとき、(lambda関数ではない)ローカル関数は一度も登場せず グローバルスコープへと到達します。 このような場合、その探索過程で遭遇するすべてのローカル変数にアクセスできます。 また、グローバルスコープにある変数(st_global_var)やstatic変数はいかなる場合もアクセス可能です。 結局すべての変数にアクセスできる形になります。
この例において途中で(lambda関数ではなく)ローカル関数(function func1)を一つ含むような別の例を考えます。 例えば次のようなものです。
/* Global Scope */
varia st_global_var int/* OK */
{ /* Global Block */
varia gblk_var int/* Cannot access. */
function func1(){ /* Local function here! */
varia local1_var int/* OK */
lambda lamb2(){
varia local2_var int/* OK */
lambda lamb3(){
/***
* Referable from here:
*/
local2_var = 9
local1_var = 9
//gblk_var = 9 /* Compile error : Cannot access */
st_global_var = 9
}
}
}
}
上記の例では、一番最内のlambda関数lamb3から出発し、その外側向かって探索したとき、まずlambda lamb2を経由し、 次に(lambda関数ではない)ローカル関数「function func1」が登場します。 このような場合、この「function func1」が登場するまでのすべてのブロック(lambda lamb2配下のブロックとfunction func1配下のブロック)で遭遇するすべてのローカル変数にアクセスできます (つまりlocal2_varとlocal1_varへアクセスできます)。 一方、この「function func1」よりも外側にあるブロック内のローカル変数(gblk_var)へはアクセスできません。 上記の例で gblk_var はグローバルブロック内にあるローカル変数ですので、lamb3からこれにアクセスすることはできないわけです。 また、グローバルスコープにある変数(st_global_var)やstatic変数はいかなる場合もアクセス可能です。 結局、この例で参照可能なのは local2_var, local1_var, st_global_var となります
さらに別の例を考えます。 次の例は途中で現れるローカル関数(function func2)の位置が前回の例とは異なります。
/* Global Scope */
varia st_global_var int/* OK */
{ /* Global Block */
varia gblk_var int/* Cannot access */
lambda lamb1(){
varia local1_var int/* Cannot access */
function func2(){ /* Local function here! */
varia local2_var int/* OK */
lambda lamb3(){
/***
* Referable from here:
*/
local2_var = 9
//local1_var = 9 /* Compile error : Cannot access */
//gblk_var = 9 /* Compile error : Cannot access */
st_global_var = 9
}
}
}
}
上記の例では、一番最内のlambda関数lamb3から出発し、その外側向かって探索したとき、 (lambda関数ではない)ローカル関数「function func2」が登場します。 このような場合、この「function func2」が登場するまでのすべてのブロック(function func2配下のブロック)で遭遇するすべてのローカル変数にアクセスできます (つまりlocal2_varへアクセスできます)。 一方、この「function func2」よりも外側にあるブロック内のローカル変数へはアクセスできません。 上記の例で local1_var は(「lambda lamb1」がlambda関数ではありますが)ローカル変数ですので、 lamb3からこれにアクセスすることはできないわけです。 また同様にgblk_var もlamb3からアクセスできません。 ただし、グローバルスコープにある変数(st_global_var)やstatic変数はいかなる場合もアクセス可能です。 結局、この例で参照可能なのは local2_var, st_global_var となります
最後に念のため、static変数を含むような別の例も見ておきましょう。
/* Global Scope */
varia st_global_var int/* OK */
{ /* Global Block */
static varia gblk_var int/* OK */
lambda lamb1(){
varia local1_var int/* Cannot access */
function func2(){ /* Local function here! */
varia local2_var int/* OK */
lambda lamb3(){
/***
* Referable from here:
*/
local2_var = 9
//local1_var = 9 /* Compile error : Cannot access */
gblk_var = 9
st_global_var = 9
}
}
}
}
上記の例では、一番最内のlambda関数lamb3から出発し、その外側向かって探索したとき、 (lambda関数ではない)ローカル関数「function func2」が登場します。 この「function func2」が登場するまでのすべてのブロック(function func2配下のブロック)で遭遇するすべてのローカル変数にアクセスできますので、 local2_varへはアクセスできます。 この「function func2」よりも外側にあるブロック内のローカル変数へはアクセスできませんが、 それがstatic変数ならばアクセスすることが可能です。 この例では、local1_varはローカル変数ですのでアクセスできませんが、gblk_varはstatic変数ですので、 今回はこれにアクセスできます。 結局、この例で参照可能なのは local2_var, gblk_var, st_global_var となります (勿論、static変数ではその実体は終始一つだけとなるため、使用可能な状況を選びます)。
ここまでをまとめますと、結局すべてlambda関数にしてしまえば問題ないわけですが、 lambda関数は呼び出しのオーバーヘッドが(ローカル関数と比べ)多少あるということ、またlambda関数の自由なアクセスそれ自体が かえって変数の値の変遷を追いにくくするデメリットもあります。そのあたりも考慮しつつ適切な方を使いましょう (そもそもこんな何重にもネストを必要とする状況それ自体が、レアなケースではありますが)。
グローバル定数
一つのソースファイルだけに限るのであれば、そのファイル全体から参照可能なグローバルな定数を宣言するのは簡単です。 以下のようにファイルの最初の方で、グローバルスコープにおいてconst型で定数を宣言します。
/* ファイルの最初の方 */
const my_const = 777
/* ファイルの中味... */
では複数のソースファイルに跨いで参照可能なグローバルな定数を宣言するにはどうすればいいでしょうか? この場合、以下のようにヘッダファイルにconst文を書きます。 そしてその定数を参照したいソースファイルはすべてそのヘッダをインポートするようにします。
/* ヘッダファイルの内部 */
const my_const = 777
C言語ではヘッダファイルにマクロやenum文で定数を定義して
(あるいはC++ならばstatic constやconstexprなどが使えますが)、
複数のソースファイルからそのヘッダをインクルードし、所望の定数を参照するようなことをしますが、
ここで行っているのもそれとほぼ同じようなことです。
関数のデフォルト引数再考
関数のデフォルト引数についてはユーザ定義関数のセクションで既にある程度述べましたが、 ここで改めてそれについて残りの詳細を述べたいと思います。
Rarakuのデフォルト引数においては基本的に任意の式を指定することができます。 ただし同じ関数の他の引数にアクセスするような式を書くことはできません。 例えば以下のような記述はできません。
function func( x int, y int=x+1 ) /* Compile error */
{
/* something */
}
func( 20 )
上記で仮引数yのデフォルト引数の値「x+1」で指定された変数 x が指し示すのは、仮引数のxではありません。 この関数内のトップブロックから始めて上へ向かって探索したときに(仮引数だけは探索の対象から除外したとして)最初に見つかる変数を指し示します。 この例では、この関数より上のスコープ、すなわちグローバルスコープに変数xがどこにも宣言されていないため、 変数xは存在しないものとみなされ、コンパイルエラーとなります。
一方、デフォルト引数の値に含まれる変数が、グローバルスコープやstatic変数として関数の外部に存在する場合はそれが参照されます。 例えば以下の通りです。
varia x = 20
function func( x int, y int=x+1 )
{
Rrk_print( \=x \, \=y \n )
}
func( 10 )
{
static varia s = 40
function my_func( x int, y int=s+1 )
{
Rrk_print( \=x \, \=y \n )
}
my_func( 30 )
}
この例の実行結果は以下のようになります。
x=10, y=21
x=30, y=41
デフォルト引数はコンパイル時に関数呼び出し側の引数(arguments)へ埋め込まれます。 そのため、もしもライブラリ側でデフォルト引数の値を変更した場合、 それを呼び出している側のコードもリコンパイルしなければその値の変更は反映されないことに注意してください (引数の型や数は合っているので一見するとライブラリ側でのリコンパイルのみで済みそうに見えること、 また実際、呼び出し側でリコンパイルしなくても一応動作するため、 このような状況では中途半端にデフォルト値の更新が反映されていることに気づきにくいかもしれません)。
ライブラリ側でのリコンパイルだけでデフォルト値の変更が確実に全体に反映されるようにするために、 以下のようにデフォルト値を返すだけの関数をライブラリ側で実装を隠蔽して作成し、 それをデフォルト引数として与えるという方法があります。
ライブラリ側の実装:
global DefaultArg_enable() bool {
return false
}
ライブラリ側のヘッダ:
global DefaultArg_enable() bool
function my_func( enable bool = DefaultArg_enable() ){
{
/* something */
}
呼び出し側:
my_func()
もちろん値が変わるようなことはまずないようなデフォルト引数(例えば文字列の範囲を表すpos=0, size=Rrk_NPOSのようなデフォルト引数)や、 1つのファイル内でのみ使用する関数(非globalな関数)でのデフォルト引数、 あるいはそもそも呼び出し側をいつでも完全にリコンパイルして運用できる環境であれば、ここまでする必要はないと思われます。 そうではない環境で、しかも将来の仕様の変更によっては値が変わる可能性が考えられるようなデフォルト引数ならば 安全のため上記の方法をとっておくのもよいでしょう。
ヘッダファイルとリンケージのまとめ
外部リンケージを持つ関数はglobalキーワードによって定義を行い、またそのプロトタイプ宣言をヘッダにおいて行います。 それを使う側のファイルやその関数を定義したファイルではそのヘッダをimportするようにします。
globalキーワードを用いずに関数を定義した場合それはすべて内部リンケージを持つ関数となります。 他のファイルから使用する必要のない関数は、無駄にglobal関数にはしないようにしましょう。
グローバルスコープで関数呼び出しが出来るのはエントリーファイルのみです。 ユーティライズファイルではこれは許可されません。
グローバル定数を宣言したい場合はヘッダファイルにおいてそれを行いましょう。 Rarakuではグローバル変数を宣言することはできませんが、そのようなことが必要な場合はglobal関数を用いてアクセサを作り、 それを介してアクセスするようにします。
内部リンケージを持つ関数のうち、そのファイル内のどこからでも呼び出し可能な関数をスタティック関数と呼びます。 functionキーワードやstaticキーワードを用いてグローバルスコープで関数を定義した場合、それはすべてスタティック関数となります。
無名ブロック内においては、staticキーワードを用いて関数を定義するとそれはスタティック関数となり、 functionキーワードを用いて関数を定義するとそれはローカル関数となります。
目次に戻る
型のキャストと暗黙変換
型のキャストとその記法
Rarakuでは基本的に同じ型同士での代入や演算が可能であり、一方で異なる型同士では一部の例外を除きそれが許可されません。 このような型による制限はプログラミングミスを防止するためのものですが、 とはいえ実際のプログラミングでは文脈上明らかに問題ない場合において、ある型を他の型に変換したり別の型とみなして処理したい場合が往々にしてあります。 そのような場合にキャストを使います。
キャストを行うにはキャスト演算子「->」を用いて「変数 -> 変更先の型名」という書式で記述します。 例えば以下の通りです。
varia i32 int32
varia i64 int64
i32 = i64->int32 /* int64型の変数をint32型へキャストし、int32型の変数へ代入 */
real型からint型へのキャストも同様に可能です。
varia ival int
varia rval real = 2.5
ival = rval->int /* real型の変数をint型へキャストし、int型の変数へ代入 */
Rrk_print( \=ival\n ) /* ivalの値を確認表示 */
ただしこの場合、小数点以下は強制的に打ち切られます。 上の例ではint型へのキャストによって「2.5」の「.5」の部分が打ち切られ、値は「2」となります(これはC言語で同様のキャストをした場合と同じ挙動です)。 この例の実行結果は以下のようになります。
ival=2
その他の数値型についても同様に相互に自由にキャストできます。
Rarakuにおいて数値型は uint8、uint16、uint32、uint64、int8、int16、int32、int64、real32、real64 の十個になります。
またintはint64、uintはuint64、realはreal64の別名です。
すべての数値型は文字列へキャストできます。 例えば以下の通りです。
varia ival int
varia str string
/***
* int型の変数をstring型へキャスト.
* 厳密にはivalを文字列に変換したstring型のGCメモリが新たに内部的に割り当てられ、
* 変数strがそのGCメモリを参照する形になる.
*/
str = ival->string
ハイプライオリティキャストとロープライオリティキャスト
これまでに登場した「->」演算子は演算の優先順位(演算の結合力)が高く、構造体のメンバアクセス演算子(ドット)などと同じレベルの優先順位を持ちます。 例えば識別子 m があり、その直後(括弧を付けず)に「->」演算子がある場合、このキャストが掛かる範囲はその識別子 m のみとなります。 つまり「-m->uint」とあった場合、このキャストが掛かる範囲は「-m」ではなく「m」です。 このキャストが掛かる範囲を「-m」にしたい場合は「(-m)->uint」のように明示的に括弧を付ける必要があります。
では識別子ではなく数値リテラルの場合どうでしょうか?
例えば「-2->uint」とあった場合、このキャストが掛かる範囲は「2」でしょうか?それとも「-2」でしょうか?
紛らわしいのでこれについてここで少しまとめておきましょう。
上記のA, B, Cにおける「-2」は、一つの数値リテラル「-2」のように見えますが、 実際はまず単項演算子「-」と「2」に分割され、その後に「-2」という数値リテラルが改めて合成されます。 キャスト演算子「->」の優先順位は単項演算子「-」よりも高いため、 「->uint」が掛かる範囲は「2」だけとなります。
紛らわしいのでこれについてここで少しまとめておきましょう。
varia A = -2->uint /* A */
varia B = 3 -2->uint /* B */
varia C = 3 - -2->uint /* C */
上記のA, B, Cにおける「-2」は、一つの数値リテラル「-2」のように見えますが、 実際はまず単項演算子「-」と「2」に分割され、その後に「-2」という数値リテラルが改めて合成されます。 キャスト演算子「->」の優先順位は単項演算子「-」よりも高いため、 「->uint」が掛かる範囲は「2」だけとなります。
一般的な用語ではありませんが、ここでは演算子「->」をハイプライオリティーキャスト(high-priority cast)と呼びます。 一方、演算子「|->」は、この優先順位を大幅に低くしたキャスト演算子です。 これをロープライオリティーキャスト(low-priority cast)と呼びます。 この演算子を使った場合、例えば「-2 |->uint」のように書くとキャストが掛かる範囲は「-2」になります。
括弧を利用したハイプライオリティーキャスト(「->」)は任意の範囲のキャストが可能ですが、 ロープライオリティーキャスト(「|->」)を使うと、特に算術式においてその括弧を省略できる場合があります。 例えば以下の通りです。
varia i = (-2)->int /* high-priority cast */
varia j = -2|->int /* low-priority cast */
varia H = ( ( (i+1)*(j+1) )->uint % 3 )->int /* high-priority cast */
varia L = (i+1)*(j+1) |->uint % 3 |->int /* low-priority cast */
上記の H と L の右辺は結果的にはどちらも同じ計算を行っており、 まず「(i+1)*(j+1)」の結果をuint型にキャストし、それを3で割った剰余をint型に変換しています。 このようにキャスト演算子の前に来る算術式が長い場合や連鎖する場合などでは、 ロープライオリティキャストの方が括弧がネストしない分、少しだけ記述が簡潔になります。
ロープライオリティキャストの優先順位は算術演算子「+」「-」よりは低いですが、 比較演算子よりは高くなっています。 そのためロープライオリティキャスト「|->」を使うのはまとまった算術式が終わった最後尾に限った方がよいでしょう。 それ以外の式ではやはりハイプライオリティーキャスト「->」と括弧を使い、キャストの掛かる範囲を明確にした方が無難と思います。
関数形式キャスト
関数形式キャスト(function-like cast)とは見かけ上関数呼び出しのように見えるキャストのことです。 ただし通常の関数呼び出しとの違いは、「タイプ名( ... )」という形式になることです。 例えば以下をご覧下さい。
varia i = (-2)->int /* high-priority cast */
varia j = -2|->int /* low-priority cast */
varia k = int(-2) /* function-like cast */
varia H = ( ( (i+1)*(j+1) )->uint % 3 )->int /* high-priority cast */
varia L = (i+1)*(j+1) |->uint % 3 |->int /* low-priority cast */
varia F = int( uint( (i+1)*(j+1) ) % 3 ) /* function-like cast */
上記の H と L と F は結果的にはどれも同じ計算を行っており、 まず「(i+1)*(j+1)」の結果をuint型にキャストし、それを3で割った剰余をint型に変換しています。
通常のキャストの場合、最後まで見ないとその括弧がただの括弧なのかキャストによる括弧なのかが判明しませんが、 関数形式キャストを使った場合、その位置からキャストが始まることが明確になる点では可読性が高いと言えます。 一方、ネストが深くなると今度は型名がその変化の順番とは逆順に並ぶため、関数呼び出しをネストするのと同じ読み辛さが発生してしまいます。
関数形式キャストで関数型をキャストすることは許可されません。
Rarakuでは関数型のタイプ名と実際の関数名を同名として定義することもできます。 このとき関数形式キャストでは、それが関数呼び出しなのか関数形式キャストなのかがいよいよ紛らわしくなり、 著しく可読性が損なわれる恐れがあります。 そのため、Rarakuでは関数型に限り、関数形式キャストの使用を禁止しています。
そもそもある関数型を別の関数型へキャストしたい状況自体がそれほど起きることではありせんが、 どうしても関数型をキャストしたい場合は替わりに「->」演算子を使いましょう。
Rarakuでは関数型のタイプ名と実際の関数名を同名として定義することもできます。 このとき関数形式キャストでは、それが関数呼び出しなのか関数形式キャストなのかがいよいよ紛らわしくなり、 著しく可読性が損なわれる恐れがあります。 そのため、Rarakuでは関数型に限り、関数形式キャストの使用を禁止しています。
そもそもある関数型を別の関数型へキャストしたい状況自体がそれほど起きることではありせんが、 どうしても関数型をキャストしたい場合は替わりに「->」演算子を使いましょう。
整数型とある程度互換性のある非数値型とのキャスト
数値型同士では割と自由にキャストが可能でしたが、数値型とそれ以外の型とのキャストの場合、 どんな型から型へも自由に行えるわけではなく、不可能なパターンがあります。
数値型をbool型へキャストすることもできますが、その場合は値が非ゼロならばtrue、ゼロならばfalseになります。 逆にbool型を数値型にキャストした場合、trueならば1、falseならば0になります。 例えば整数値 2 を bool 型へキャストすれば true となり、それを再び整数型にキャストすれば 1 になります。 このとき元の 2 という情報は失われて 1 になってしまいますので、この変換は不可逆なものとなります。
enum型を整数値へキャストしたり、またその逆にenum型へキャストすることも可能です。 これについては「列挙型」のセクションで詳しく説明しています。
handler型は実体はuint64型の整数を型として独立させたものです。 よってこれはuint64型(と特例としてbool型)へキャストできますが、その他の型へはキャストできません。 また逆にuint64型をhandler型へキャストすることも可能ですが、一方でbool型からhandler型へのキャストはできません。 例えば以下の通りです。
handler MyHandler
varia hndl MyHandler
varia u64 uint64 = 10
varia u uint = 10
varia u32 uint32 = 10
varia i64 int64 = 10
varia bval bool = true
u64 = hndl->uint64 /* OK */
u = hndl->uint /* OK( uint is the same as uint64) */
bval = hndl->bool /* OK */
//u32 = hndl->uint32 /* NG */
//i64 = hndl->int64 /* NG */
hndl = u64 ->MyHandler /* OK */
hndl = u ->MyHandler /* OK( uint is the same as uint64) */
//hndl = bval->MyHandler /* NG */
//hndl = u32 ->MyHandler /* NG */
//hndl = i64 ->MyHandler /* NG */
上記はあくまで例のためのかなり乱暴なコードですが、このようなhandler型に関するキャストは(C言語のAPIとの連携をどうしてもとりたいなど)やむを得ない特殊な用途に留めるべきです。
また結局のところuint64を媒介としてキャストを連鎖させることで任意の数値型とハンドラ型との間で強引にキャストすることも一応可能でありますが、
このようなキャストも勿論、通常はすべきではありません。
構造体型や配列型に関するキャスト
構造体や配列から数値型やbool型へキャストしたり、その逆をするなどといったことはできません。
ただしif文の条件式として使う場合のみ、構造体や配列をbool値として扱えます。
このときキャストは必要ありません。
これは構造体や配列がnullの場合に false となり、そうではない(実体が確保されている場合)に true となります。
現段階のRarakuではキャスト演算子に [ ] が付加された型を指定することはそもそもできません。 例えば以下はコンパイルエラーとなります。
varia a int32[]
varia b = a->int32[] /* コンパイルエラー: このフォーム自体が現段階のRarakuパーザでは認められていない */
例えばuint64[] から uint32[] のように明らかに要素のバイトサイズが異なる場合であればともかく、
int64[] から uint64[] のような配列の場合、
結局は高々符号の有り無しの差ですから「->uint64[]」といったような記述ができても問題ないような気もしますが、
とりあえず現状ではこれは問答無用で不可としています。
必要ならば将来ビルトイン関数を用意することでこのようなキャストをサポートするかもしれません。
尚、array文で別名を付けた型名をキャスト演算子のオペランドとすることは可能です。
ただしその場合はキャスト先の型もそれと一致している必要があります。
構造体から構造体へのキャストは可能な場合と不可能な場合があります。 これについては後述します。
数値型の暗黙変換
異なる型同士であってもキャストを使うことなく例外的に代入ができる場合があります。 これを型の暗黙変換と呼びます。
言語仕様の背景: 型の暗黙変換を排除すべきか?
型の暗黙変換を一切認めず排除する仕様も考えられますが、これは本当に安全でしょうか?
型の暗黙変換がない場合、結局のところキャストを連発することになります。 それでも適切にキャストしている間はよいのですが、あまりにキャストの量が多いとそのうち考えるのも面倒になり、 本来する必要のないキャストを惰性や勢い余ってしてしまうことでかえって型安全性を欠く (潜在的な危険性がキャストによって揉み消される)シナリオも考えられます。 極端な方向へ行き過ぎるのはやはりあまりよくないように思えます。
数値型の型変換は複雑であり、どのような問題や注意点があるのか、 プログラミングにおいてそれを人間がある程度把握しておくことはどのみち避けて通れない道です。 一方で、機械で自動的にできることは自動的に行わせることもまた重要です。
Rarakuでは型の暗黙変換をなるべくC言語に準じた形で導入しています。 C言語の暗黙変換にも色々と問題はありますが、一方でその知見が十分界隈で共有されていることが強みとなります。 C言語の暗黙変換の中であからかまに問題があったり意味不明と思われるものはRarakuでも採用していませんが、 例えばuint32_t型からuint64_t型への代入など明らかに問題のない暗黙変換も多くあります。 その知見をそのまま利用できる形にしておいた方が総合的に見て今の段階ではまだ安全であると我々は考えます。
Close
型の暗黙変換がない場合、結局のところキャストを連発することになります。 それでも適切にキャストしている間はよいのですが、あまりにキャストの量が多いとそのうち考えるのも面倒になり、 本来する必要のないキャストを惰性や勢い余ってしてしまうことでかえって型安全性を欠く (潜在的な危険性がキャストによって揉み消される)シナリオも考えられます。 極端な方向へ行き過ぎるのはやはりあまりよくないように思えます。
数値型の型変換は複雑であり、どのような問題や注意点があるのか、 プログラミングにおいてそれを人間がある程度把握しておくことはどのみち避けて通れない道です。 一方で、機械で自動的にできることは自動的に行わせることもまた重要です。
Rarakuでは型の暗黙変換をなるべくC言語に準じた形で導入しています。 C言語の暗黙変換にも色々と問題はありますが、一方でその知見が十分界隈で共有されていることが強みとなります。 C言語の暗黙変換の中であからかまに問題があったり意味不明と思われるものはRarakuでも採用していませんが、 例えばuint32_t型からuint64_t型への代入など明らかに問題のない暗黙変換も多くあります。 その知見をそのまま利用できる形にしておいた方が総合的に見て今の段階ではまだ安全であると我々は考えます。
Close
まず数値型の場合、これは基本的に代入先となる左辺の型が右辺の型よりも扱える範囲が大きい場合に可能となります。
これは集合論の記号で書けば、「左辺の型 ⊇ 右辺の型」と表現できます。
またRarakuではこのことを大小比較演算子を用いて「左辺の型 >= 右辺の型」というように表現する場合があります。
例えば以下の通りです。
varia i32 int32
varia i64 int64
i64 = i32 /* int32型の変数からint64型への代入は普通に可能 */
int32で許容可能な値の範囲はint64でも例外なく許容できるため、 このような代入をしても問題は起きないわけです。 一方、その逆方向の代入、すなわちint64からint32への代入では先ほどの例のようにキャストが必要になります。 右辺となるint64変数の側にint32変数では許容不可な値が入っている可能性を排除できないためです。 これは例えばint32で表現できる最大値2147483647を超える大きすぎる正値や、その逆の小さすぎる負値です(通常そのような値を扱うことはあまりないかもしれませんが)。
上記の例は、集合論の記号で「int64 ⊇ int32」と表現できます。
また大小比較演算子で「int64 >= int32」と表現できます。
通常のプログラミングではほとんど意識する必要はありませんが、int32型の負の数からint64型の負の数へ代入する場合、Rarakuでは(一般的な)C言語と同様のbitの変化が起こります。
例えばint32型としての-1は、bitとして16進数で表すとff ff ff ffとなっています。
これをそのままint64型の変数へ代入した場合、負値-1としての値は維持されますが、
これをbitとして16進数で表すと ff ff ff ff ff ff ff ff となります。
つまり左端にffが4つ自動的に追加される形になります。
一方、uint32型からuint64型へ代入する場合、例えばuint32型として ff ff ff ff となっていて、 これをそのままuint64型の変数へ代入した場合、00 00 00 00 ff ff ff ff となります。 これは(int32/int64の時の対比として)左端に00が4つ自動的に追加される形になると考えてもよいでしょう。
これはint型とuint型を跨ぐ形でキャストしても同様で、まとめると以下のようになります。
Rarakuでは単独で現れる int8/int16/int32/int64 のすべての変数やリテラルを内部的にはC言語におけるint64_tで管理しています。
ただしこれらの配列は別で、それぞれ(C言語の意味で)本当にint8_t、int16_t、int32_t、int64_tの配列になっています。
一方、uint32型からuint64型へ代入する場合、例えばuint32型として ff ff ff ff となっていて、 これをそのままuint64型の変数へ代入した場合、00 00 00 00 ff ff ff ff となります。 これは(int32/int64の時の対比として)左端に00が4つ自動的に追加される形になると考えてもよいでしょう。
これはint型とuint型を跨ぐ形でキャストしても同様で、まとめると以下のようになります。
- 変換先がintであり、かつbit幅が広がる場合 変換元の最上位ビットが1の場合、変換先の上位ビット側に ff が追加される。 変換元の最上位ビットが0の場合、変換先の上位ビット側に 00 が追加される。
- 変換先がuintであり、かつbit幅が広がる場合 上位ビット側に 00 が追加される。
- 変換先がintであり、かつbit幅が縮む場合 上位ビット側がカットされる。
- 変換先がuintであり、かつbit幅が縮む場合 上位ビット側がカットされる。
宣言における暗黙変換
宣言時に明示的に型を指定し、かつ右辺の値がその型とは異なる場合、自動的に暗黙変換が行われます。 例えば以下の通りです。
varia u64 uint64 = 10 /* A */
varia u uint = 10 /* B */
varia u32 uint32 = 10 /* C */
varia i32 int32 = 10 /* D */
一見すると何の変哲もない宣言に見えますが、Rarakuでは数値リテラル「10」は本来はint64型です。 そのため、上記の例は厳密には右辺の型はどれも宣言の型とは一致していませんが、 「10」という数字はuint64、uint、uint32、int32のどの型でも問題なく含むことができる値です。 そのためこの場合は「10」は明示的に宣言された型に暗黙変換されます。
配列の暗黙変換
配列の場合、これは基本的に代入先となる左辺の型が右辺の型よりも不変性(イミュータブル)の度合いが大きい場合に可能となります。 不変性が大きいということは、不変なものと変更可能なものの両方を扱えるということで、 やはり扱える範囲としては大きいことになります。
具体的には「配列とモディファイア」のセクションでのアサイナビリティの説明でも述べましたが、 例えば可変長配列同士の代入では、左辺がconst配列やtight配列のような右辺より制限が厳しい配列である場合、 その代入が可能です。 また固定長配列同士の代入では、要素数の多い方から要素数の少ない方(要素数が少ない分、逆に不変性が大きい)への代入が可能です。 固定長配列と可変長配列が混在する場合、それらは基本的に相互に代入できませんが、 constやtightモディファイアが付加された制限の厳しい可変長配列へ固定長配列を代入することができる場合があります。
上記は例えば、集合論の記号で「const int[] ⊇ vaira int[]」や「int[2] ⊇ int[3]」と表現できます
(なんとなく int[3] の方が大きい集合のような気がしてしまいますが、これは型としてカバーできる範囲を表しますので、
int[2] の方が大きい集合となります)。
また大小比較演算子で「const int[] >= varia int[]」や「int[2] >= int[3]」と表現できます。
数値型では、例えばuint32型からuint64型への自然な代入が可能でした。 しかし配列では、例えばuint32[]型からuint64[]型への代入は不可能となります。 Rarakuでの配列の代入ではその要素の型は双方一致していなければなりません。
構造体の暗黙変換とダウンキャスト
構造体の場合、これは基本的に代入先となる左辺の型が右辺の型よりも不変性(イミュータブル)の度合いが大きい場合に可能となります。 不変性が大きいということは、不変なものと変更可能なものの両方を扱えるということで、 やはり扱える範囲としては大きいことになります。
具体的には「構造体」のセクションでも述べましたが、左辺値の構造体のメンバの型の並びが、右辺値の構造体のメンバの型の並びに完全に含まれており、 かつその出だしの並びも一致している場合、キャスト演算子「->」と用いることでこれを代入できます。 「構造体」のセクションで挙げた例を以下に再掲します。
struct Struct1 {
i int
r real
}
struct Struct2 {
i int
r real
c conststr
}
varia s1 Struct1 = { 10, 20.5, }
varia s2 Struct2 = { 30, 40.5, "hello" }
s1 = s2->Struct1 /* OK */
//s2 = s1->Struct2 /* NG */
上記において、Struct1の方ではメンバの型の並びは int, real、Struct2の方では、int real, conststr となっており、 Struct1の全序列( int real )が Struct2 の開始の序列と一致しています。
このような型の組み合わせの場合、キャスト演算子を用いてStruct2型の変数s2をStruct1型へ変換し、その上でStruct1型の変数s1へ代入することができます。
これが一般的に許可される理由は、代入先であるs1(Struct1型)のどのメンバにアクセスしたとしても、 それは代入元のStruct2のメンバとしても含まれているためです。 またStruct1からはStruct2のメンバcを変更することはできず、その意味では不変性(イミュータブル)の度合いとしてStruct1の方が大きいことになります。
上記は、集合論の記号で「Struct1 ⊇ Struct2」と表現できます。
また大小比較演算子で「Struct1 >= Struct2」と表現できます。
一方、この逆の代入(上記の例の場合 s2 = s1->Struct2)はコンパイルエラーとなります(正確には代入以前に「s1->Struct2」の時点でコンパイルエラーとなります)。
これが一般的に許可されない理由は、代入先であるs2(Struct2型)では、(その型の形式上)代入元(Struct1)に存在しないメンバ(c)へのアクセスが起こり得るためです。 仮にs1が3番目のメンバcが存在しないような実体を指し示していた場合、それ以降の処理でそれにアクセスした瞬間、明らかに支障が生じます。 一方、s1が3番目のメンバcが存在するような実体を指し示している可能性もあります。 ただそのどちらなのかは実行時になるまでわからず、従ってコンパイルの段階でそれを一般的に判断する方法はありません。 そのためコンパイル段階ではこれを安全側に振って判断し、通常このようなキャストを許可しません。
とはいえ、実行時に本当にs1が「3番目のメンバcが存在するような実体」を指し示しているならばそのような代入は実質的には可能なわけです。 そして実際の応用ではそのような場合に限って代入を行い、そうでないならば代入は行わずに代替の処理等を行うようにしたい場合があります。 そのためにはこの判定を(コンパイル時ではなく)実行時に動的に行わなければなりません。 Rarakuでこれを実現するには、まず二つ以上の構造体間で親子関係をもたせ、さらにダウンキャスト演算子「^downcast」を使う必要があります。
まず構造体に親子関係をもたせる方法から説明しましょう。 そのためには「struct 自分の識別子 : 親の識別子 { .... }」という書式でstruct文を記述します。 「自分の識別子」の後ろに「:」を付け、その後ろに親となる構造体の識別子を記述することで親子関係が定義されるわけです。
念のために補足しますが、あくまでこのstruct文では「自分の識別子」という名前の構造体を定義しているのであって、 「親の識別子」という名前の構造体を定義しているのではありません。 親となる方の構造体はまた別のstruct文で定義しておく必要があります。 例えば以下のようになります。
struct Struct1 {
i int
r real
}
struct Struct2 : Struct1 { /* Struct2の親はStruct1 */
i int
r real
c conststr
}
ただしどんな構造体も自由に親として選択出来るわけではありません。 これが可能なのは親の構造体のメンバの型の並びが、子の構造体のメンバの型の並びに完全に含まれており、 かつその出だしの並びも一致している場合に限られます。 実際、上の例でも両者はそのような関係になっています。
つまりこれは次のように言い換えられます。
二つの構造体AとBがあり、キャスト演算子「->」と用いることでBからAへの変換が可能なとき、かつそのときに限り、
Aを親に、Bをその子とすることができるということです。
このように構造体における「通常のキャストが可能な条件」と「親子関係が結べる条件」は全く同じであり、
それは子から親へ上がるような方向であるため、通常の「->」によるキャストを特にアップキャストと呼ぶこともあります。
その意味で、アップキャストとダウンキャストはちょうど逆の変換となります。
尚、一度親子関係をもたせたならば、キャスト演算子「->」を使わずとも、子から親への代入が可能となります。 ただしその逆の代入、つまり親から子への代入は(何度も繰り返しになりますが)ダウンキャスト演算子「^downcast」が必要になります。
ダウンキャストは「子の型の変数 ^downcast 親の型の変数」といった書式で記述します。 例えば以下のようになります。
struct Struct1 {
i int
r real
}
struct Struct2 : Struct1 { /* Struct2の親はStruct1 */
i int
r real
c conststr
}
varia s1 Struct1 = { 10, 20.5, }
varia s2 Struct2^? = { 30, 40.5, "hello" }
s2 = s1^downcast Struct2 /* 親(s1)から子(s2)へダウンキャスト */
if s2 { /* 成功しているか否かをチェックする必要がある */
Rrk_print( "Downcast success : s2 is not null."\n )
Rrk_print( "s2.c=" s2.c \n )
} else {
Rrk_print( "Downcast failure : s2 is null."\n )
}
上の例では「struct Struct2 : Struct1」とある部分で、Struct1がStruct2の親として指定されています。 また「s2 = s1^downcast Struct2」とある部分がダウンキャストですが、ここで s1 に代入されている値(s1が指し示す実体)の型をプログラムの実行時に動的に調べます。 そしてその実体の型が、代入先である s2 の型Struct2と同じ構成であるかその子構造体である場合、このダウンキャストは成功してs1が指し示している実体を返します。 そうではない場合、このダウンキャストは失敗して null を返します。
このときs2はnullにもなり得るため、s2の宣言の型もnullableとなっています。
このようにダウンキャストは失敗する場合があることに注意してください。 成功するか否かはコンパイル時にはわからず、実行時において初めてわかります。 そのため、通常ダウンキャストを行ってその結果を変数に代入した後は、その変数の値がnullかどうかを if 文等で調べなければなりません。
実際、上の例でも結果が格納された変数s2の値をif文でチェックし、その成否によって処理を分岐しています。 もっとも上の例では、コード上からその結果がどうなるかはわかります。 元々 s1 が指し示している実体は 「{ 10, 20.5, }」で、これは Struct1のインスタンス(Struct1 が実体化したもの)ですから、 Struct2へのダウンキャストは失敗しその値はnullになります(s2にはnullが代入されることになります)。
ダウンキャストが成功する例も見ておいた方がわかりやすいでしょう。 例えば以下のようになります。
struct Struct1 {
i int
r real
}
struct Struct2 : Struct1 { /* Struct2の親はStruct1 */
i int
r real
c conststr
}
struct Struct3 : Struct2 { /* Struct3の親はStruct2 */
i int
r real
c conststr
j int
}
varia s1 Struct1^? = null
varia s2 Struct2^? = { 30, 40.5, "hello" }
varia s3 Struct1 = { 10, 20.5, "world", 50 }
s1 = s3 /* s1 が指し示している実体はs3(Struct3のインスタンス) */
s2 = s1^downcast Struct2 /* 親(s1)から子(s2)へダウンキャスト */
if s2 {
Rrk_print( "Downcast success : s2 is not null."\n )
Rrk_print( "s2.c=" s2.c \n )
} else {
Rrk_print( "Downcast failure : s2 is null."\n )
}
今度の例では、元々 s1 が指し示している実体は s3(さらに言えば { 10, 20.5, "world", 50} になりますが)で、これは Struct3のインスタンスです。 つまり実質的には子から親への代入が発生していることになりますからStruct2へのダウンキャストは成功し、s2 には s1 (中味はs3)が代入されることになります。
ダウンキャスト演算子「^downcast」はいかなる構造体同士でも行えるわけではなく、 親子関係のある構造体間に限られます(さらにそれが成功するか失敗するかはまた別の話です)。 さらに親子関係があったとしてもそれが「子から親」への変換である場合、 Rarakuではこれはコンパイルエラーとなり、ダウンキャスト演算子を使わないよう促されます。 そのケースではそもそもキャストが必要ないからです。
一般にダウンキャストはその成否を静的にはチェックできないため、不用意に使うべきではありません。 避けられるケースでは極力避け、また使用する場合はその成否を必ずチェックすべきでしょう。
「->」を使った通常のキャスト(アップキャスト)はその成否がコンパイル時に静的(static)に判明します。
そのため、これをスタティックキャストと呼ぶこともあります。
一方、「^downcast」を使ったダウンキャストは上述のように実行時に成否が判明するため、
これをダイナミックキャストと呼ぶこともあります。
二項演算子における整数型の暗黙変換
Rarakuの整数型はsigned整数型とunsigned整数型に大別されます。 signed整数型は int8, int16, int32, int64, int、unsigned整数型は uint8, uint16, uint32, uint64, uint となります。 ここではこれらを二項演算子で演算した場合、signed整数型とunsigned整数型のどちらに解釈されるか、 あるいはそもそもそのような組み合わせの演算が許可されるか否かを見ます。
二項演算子とは「オペランド」「演算子」「オペランド」という形の演算のことでした。
整数型に適用できる二項演算子は例えば四則演算、剰余演算、べき乗などの算術演算子、ビット演算子があります。
Rarakuでは一部の例外を除き、基本的に以下のような規則でsigned整数型とunsigned整数型の演算が行われます。
- signed整数型同士の演算 この演算は可能です。 その結果はsigned整数型となります。 bit数が異なる場合、大きい方へ合わせて拡大されます。
- unsigned整数型同士の演算 この演算は可能です。 その結果はunsigned整数型となります。 bit数が異なる場合、大きい方へ合わせて拡大されます。
- 片方がsigned整数型、もう片方がunsigned整数型でどちらもリテラルではない場合 基本的にこれは許可されずコンパイルエラーとなります。
- 片方がsigned整数型、もう片方がsigned整数型リテラルである場合 この演算は可能です。 その結果はsigned整数型となります。 bit数が異なる場合、大きい方へ合わせて拡大されます。
- 片方がunsigned整数型、もう片方が非負のsigned整数型リテラルである場合 この演算は可能です。 その結果はunsigned整数型となります。 bit数が異なる場合、大きい方へ合わせて拡大されます。
- 片方がunsigned整数型、もう片方が負のsigned整数型リテラルである場合 この演算は可能です。 その結果はsigned整数型となります。 ただし加算、減算の場合、Rarakuでは特別にこの結果はunsigned整数型となります(これについてはすぐ後で述べます)。 bit数が異なる場合、大きい方へ合わせて拡大されます。
あるいは左オペランドの型が結果の型の決定要因となるものもあります。 例えばべき乗演算子「^^」、シフト演算子「^b_shift_l、^b_shift_r」、剰余演算子「%」などです。
演算が許可されない組み合わせについては、キャスト演算子を使ってどちらか一方のオペランドの型をもう一方の型へ明示的にキャストしなければなりません。 例えば以下の通りです。
varia i1 int = 1
varia u1 uint = 1
//i1 * u1 /* Compile error : signed整数とunsigned整数の積である */
i1 * u1->int /* OK: signed整数と signed整数の積である(結果はsigned整数) */
i1->uint * u1 /* OK: unsigned整数とunsigned整数の積である(結果はunsigned整数) */
加算、減算における整数型の特別な暗黙変換
一つ前の項目では、整数型の二項演算子における基本的なルールを見ました。 Rarakuでは加算、減算の場合、さらに特別な暗黙変換が存在します。
片方がunsigned整数、もう一方が負のsigned整数型リテラルの場合、基本的な二項演算子のルールでは結果はsigned整数型となるのでした (例えば積演算子「*」や商演算子「/」の場合などです)。 しかし加算演算子「+」と減算演算子「-」の場合、この結果はunsigned整数型となります。 例えば以下の通りです。
varia u1 uint = 3
varia u2 = u1 + -2 /* OK: unsigned整数と負の数値リテラルの加算であり、結果はunsigned整数(この例では1) */
varia u4 = -2 + u1 /* OK: 負の整数リテラルとunsigned整数の加算であり、結果はunsigned整数(この例では1) */
varia u3 = u1 - -2 /* OK: unsigned整数と負の数値リテラルの減算であり、結果はunsigned整数(この例では5) */
あと紛らわしいのがunsigned整数からunsigned整数を引き算した結果ですが、これはunsigned整数になります。 signed整数ではありませんのでご注意ください。 例えば以下の通りです。
varia u1 uint = 3
varia u2 uint = 2
varia u3 = u1 - u2 /* OK: unsigned整数とunsigned整数の減算であり、結果はunsigned整数(この例では1) */
これらの結果は算術的には負値にもなり得ますが、その場合はアンダーフローしたunsigned整数値となります。
べき乗における整数型の暗黙変換
オペランドが整数のみからなるべき乗演算子「^^」の場合、 左オペランド(つまりべき乗における底の部分)がunsignedとなる場合では結果はunsigned、 左オペランドがsignedとなる場合では結果はsignedとなります。
ちなみに左オペランドまたは右オペランドの少なくとも一方が実数型である場合、結果はreal型となります。
以下にいくつかの例を見ます。
varia u1 uint = 3
varia u2 = u1^^(2->uint) /* OK: 両オペランドともにunsigned整数である(結果はunsigned) */
varia u1 uint = 3
varia u2 = u1^^2 /* OK: 左オペランド(底)がunsigned整数である(結果はunsigned) */
varia u1 uint = 3
varia u2 = 2^^u1 /* OK: 左オペランド(底)がsigned整数である(結果はsigned) */
varia u1 uint = 3
varia u2 = u1^^-2 /* OK: 左オペランド(底)がunsigned整数である(結果はunsigned) */
varia u1 uint = 3
varia u2 = 0x3^^u1 /* OK: 左オペランド(底)がunsigned整数型リテラルである(結果はunsigned) */
次に左オペランド(つまりべき乗における底の部分)がsignedとなる場合です。
varia i1 int = -2
varia i2 = i1^^2 /* OK: 両オペランドともにsigned整数である(結果はsigned) */
varia i1 int = -2
varia i2 = i1^^0x3 /* OK: 左オペランド(底)がsigned整数である(結果はsigned)*/
varia u2 = 2^^0x3 /* OK: 左オペランド(底)がsigned整数型リテラルである(結果はsigned) */
両方のオペランドがともに数値リテラルである場合、これらはコンパイル時に静的に計算され、
Raraku仮想マシン命令のレベルでは最終的にべき乗演算命令として残らず、
つまり、あたかも計算後の数値リテラルが最初から直接記述されていたかのように最適化されます。
RarakuVMではUInt型(unsigned整数型)とInt型(signed整数型)の情報は保持しているため、
整数のべき乗については一つの命令でそれらを自動的に切り分けて実行されます。
ビットシフト演算における整数型の特別な暗黙変換
二項演算子の範疇にあるビット演算(^b_or、^b_and、^b_xor)の場合もまた、整数型の二項演算子における基本的なルールに従います。 しかしビットシフト演算(^b_shift_l、^b_shift_r)の場合は例外的に、結果は左オペランドの型がそのまま演算結果の型になります。 この点に関してはべき乗演算子と似ていますが、ビットシフト演算では実数型を指定することはできません。
剰余演算子における整数型の暗黙変換
Rarakuの剰余演算子「%」の挙動は基本的にはC言語の規定に従います。 といってもこのC言語の「%」の規定自体が割と複雑ですので、ここでは一旦それについて述べます。
「a % b」とあった場合で a と b が両方とも正の数の場合は問題ないでしょう。 「a ÷ b」をした結果の余りが計算結果となります。
問題は負の数を含む場合ですが、C言語での「%」は、基本的に左オペランドで与えられた値が正の場合は結果も正、 左オペランドで与えられた値が負の場合は結果も負となります。 また右オペランドで与えられた値は、基本的には結果の正負に影響しません。
よって負の数を踏まえて考えると、数学的には「( a / |a| ) * ( |a| % |b| )」のような計算が行われることになります。 つまりまず左オペランドの正負を見て結果の符合を求め、 次に左オペランドの絶対値と右オペランドの絶対値について普通に正の数として剰余を求め、 最後にそれに符号を付けたものが最終的な計算結果になるということです。
尚、オペランドが実数の場合、C言語ではfmod関数を使いますがRarakuでは「%」演算子がそのまま使えます。
参考: C言語での剰余演算子のさらなる挙動
上記で述べたのが一応C99でも規定されている「%」演算子の基本ルールとなります。
しかし実際のC言語の「%」では厄介なことに、この基本ルールを「見かけ上」逸脱するパターンがいくつかあります。 例えば以下をご覧下さい。
rem_test_ui16での結果はこちらの期待通り 9 です(基本ルール通りです)。 しかしrem_test_ui32、rem_test_ui64での結果は 129 となり、これはおそらく期待通りの結果ではないでしょう。 しかもこれら3つのテストには本質的な差はなく、単に16bit整数、32bit整数、64bit整数へと自然に置き換えただけのコードに見える上、 リテラルとして与えた右辺値も左辺値の型の範囲内に収まっており、特に不自然というわけでもありません。
rem_test_ui16では、a と b が一旦int型に変換され、その上で % 演算子が計算されます。 ところがrem_test_ui32では、int32_tがuint32_t型に変換され、その上で % 演算子が計算されます。 またrem_test_ui64でも同様に、int64_tがuint64_t型に変換され、その上で % 演算子が計算されます。 つまり16bitから32bitになった瞬間、(おそらくCPUの都合で)突然変換の仕方が大きく変わるため、このようにかなり統一性がない挙動になります。
もう一つ例を見ましょう。
rem_test_iu16での結果はこちらの期待通り -10 です(基本ルール通りです)。 しかしrem_test_iu32での結果は 6、rem_test_iu64での結果は 117 となり、これはおそらく期待通りの結果ではないでしょう。 しかも先ほどの例よりさらに変な値となっています。
rem_test_iu16では、a と b が一旦int型に変換され、その上で % 演算子が計算されます。 ところがrem_test_iu32では、int32_tがuint32_t型に変換され、その上で % 演算子が計算されます。 またrem_test_iu64でも同様に、int64_tがuint64_t型に変換され、その上で % 演算子が計算されます。
先ほどの例と比べてここでさらに問題なのは、変換されるint32_tやint64_tが % 演算の左オペランドであり、 これは(基本ルールによれば)符号を決める決定要因であるということです。 つまり、左オペランドがsigned整数型として正式に負であるにも関わらず、 結果が正になるといったあからさまに基本ルールに反したことが起こります。 ただしこの挙動はコンパイラによっても異なるかもしれません(筆者の環境ではVCとGCCで確認しています)。
C言語の「%」のこのような奇妙な挙動は混乱をもたらす上、これがないと困るような状況もほぼ想定できないので、 Rarakuでも流石にこれは不採用としています。 上記の例をRarakuで書いた場合、rem_test_ui16、rem_test_ui32、rem_test_ui64 ではすべて 9、 rem_test_iu16、rem_test_iu32、rem_test_iu64 ではすべて -10 となり、基本ルール通りとなります。
Close
しかし実際のC言語の「%」では厄介なことに、この基本ルールを「見かけ上」逸脱するパターンがいくつかあります。 例えば以下をご覧下さい。
/* C lang % test1 */
void rem_test_ui16(){
uint16_t a = 129u;
int16_t b = -10;
printf( "ans=%d\n", a % b ); /* 9 */
}
void rem_test_ui32(){
uint32_t a = 129u;
int32_t b = -10;
printf( "ans=%d\n", a % b ); /* 129 !? */
}
void rem_test_ui64(){
uint64_t a = 129u;
int64_t b = -10;
printf( "ans=%d\n", a % b ); /* 129 !? */
}
rem_test_ui16での結果はこちらの期待通り 9 です(基本ルール通りです)。 しかしrem_test_ui32、rem_test_ui64での結果は 129 となり、これはおそらく期待通りの結果ではないでしょう。 しかもこれら3つのテストには本質的な差はなく、単に16bit整数、32bit整数、64bit整数へと自然に置き換えただけのコードに見える上、 リテラルとして与えた右辺値も左辺値の型の範囲内に収まっており、特に不自然というわけでもありません。
rem_test_ui16では、a と b が一旦int型に変換され、その上で % 演算子が計算されます。 ところがrem_test_ui32では、int32_tがuint32_t型に変換され、その上で % 演算子が計算されます。 またrem_test_ui64でも同様に、int64_tがuint64_t型に変換され、その上で % 演算子が計算されます。 つまり16bitから32bitになった瞬間、(おそらくCPUの都合で)突然変換の仕方が大きく変わるため、このようにかなり統一性がない挙動になります。
もう一つ例を見ましょう。
/* C lang % test2 */
void rem_test_iu16(){
int16_t a = -10;
uint16_t b = 129;
printf( "ans=%d\n", a % b ); /* -10 */
}
void rem_test_iu32(){
int32_t a = -10;
uint32_t b = 129;
printf( "ans=%d\n", a % b ); /* 6 !? */
}
void rem_test_iu64(){
int64_t a = -10;
uint64_t b = 129;
printf( "ans=%d\n", a % b ); /* 117 !? */
}
rem_test_iu16での結果はこちらの期待通り -10 です(基本ルール通りです)。 しかしrem_test_iu32での結果は 6、rem_test_iu64での結果は 117 となり、これはおそらく期待通りの結果ではないでしょう。 しかも先ほどの例よりさらに変な値となっています。
rem_test_iu16では、a と b が一旦int型に変換され、その上で % 演算子が計算されます。 ところがrem_test_iu32では、int32_tがuint32_t型に変換され、その上で % 演算子が計算されます。 またrem_test_iu64でも同様に、int64_tがuint64_t型に変換され、その上で % 演算子が計算されます。
中で起きている計算が非常にわかりにくいため、補足が必要でしょう。
-1をuint32_t型にしますと4294967295(uint32_t型の最大値)となり、 その mod 129 を取る(129で割った余りを求める)と 15 となります。 -10 = -1 - 9 ですから、15 - 9 = 6 が「-10をuint32_t型した上で 129 で割った余り」となります。
同様に、-1をuint64_t型にしますと18446744073709551615(uint64_t型の最大値)となり、 その mod 129 を取る(129で割った余りを求める)と 126 となります。 -10 = -1 - 9 ですから、126 - 9 = 117 が「-10をuint64_t型した上で 129 で割った余り」となります。
-1をuint32_t型にしますと4294967295(uint32_t型の最大値)となり、 その mod 129 を取る(129で割った余りを求める)と 15 となります。 -10 = -1 - 9 ですから、15 - 9 = 6 が「-10をuint32_t型した上で 129 で割った余り」となります。
同様に、-1をuint64_t型にしますと18446744073709551615(uint64_t型の最大値)となり、 その mod 129 を取る(129で割った余りを求める)と 126 となります。 -10 = -1 - 9 ですから、126 - 9 = 117 が「-10をuint64_t型した上で 129 で割った余り」となります。
先ほどの例と比べてここでさらに問題なのは、変換されるint32_tやint64_tが % 演算の左オペランドであり、 これは(基本ルールによれば)符号を決める決定要因であるということです。 つまり、左オペランドがsigned整数型として正式に負であるにも関わらず、 結果が正になるといったあからさまに基本ルールに反したことが起こります。 ただしこの挙動はコンパイラによっても異なるかもしれません(筆者の環境ではVCとGCCで確認しています)。
ちなみに標準数学関数であるfmodに与えた場合は、概ね基本ルール通りの結果となるようです。
C言語の「%」のこのような奇妙な挙動は混乱をもたらす上、これがないと困るような状況もほぼ想定できないので、 Rarakuでも流石にこれは不採用としています。 上記の例をRarakuで書いた場合、rem_test_ui16、rem_test_ui32、rem_test_ui64 ではすべて 9、 rem_test_iu16、rem_test_iu32、rem_test_iu64 ではすべて -10 となり、基本ルール通りとなります。
/* Raraku lang % test */
function rem_test_ui16(){
varia a uint16 = 129u
varia b int16 = -10
Rrk_print( "ans=" a % b \n ) /* 9 */
}
function rem_test_ui32(){
varia a uint32 = 129u
varia b int32 = -10
Rrk_print( "ans=" a % b \n ) /* 9 */
}
function rem_test_ui64(){
varia a uint64 = 129u
varia b int64 = -10
Rrk_print( "ans=" a % b \n ) /* 9 */
}
function rem_test_iu16(){
varia a int16 = -10
varia b uint16 = 129u
Rrk_print( "ans=" a % b \n ) /* -10 */
}
function rem_test_iu32(){
varia a int32 = -10
varia b uint32 = 129u
Rrk_print( "ans=" a % b \n ) /* -10 */
}
function rem_test_iu64(){
varia a int64 = -10
varia b uint64 = 129
Rrk_print( "ans=" a % b \n ) /* -10 */
}
rem_test_ui16()
rem_test_ui32()
rem_test_ui64()
rem_test_iu16()
rem_test_iu32()
rem_test_iu64()
Close
比較演算子における暗黙変換
型のキャストと暗黙変換のまとめ
キャストを行うにはキャスト演算子「->」を用います。 数値型同士では割と自由にキャストが可能ですが、数値型とそれ以外の型とのキャストの場合、不可能なパターンがあります。 構造体や配列から数値型やbool型へキャストしたり、その逆をすることはできません。 構造体同士の代入ではアップキャストが可能なケースとダウンキャストが可能なケースの違いにも注意しましょう。
数値型の場合、基本的に代入先となる左辺の型が右辺の型よりも制限として緩い場合に暗黙変換可能ですが、 一方、配列と構造体の場合、逆に制限として厳しい場合に暗黙変換可能です。
二項演算子における整数型の暗黙変換では、基本的にunsigned同士なら結果はunsigned、signed同士なら結果はsignedとなります。 片方がunsigned、もう一方がsignedである場合、基本的にこのまま直接演算することは許可されず、キャストが必要となります。 ただし数値リテラルが絡む場合や加算、減算の場合、特別に(キャストなしで)演算が許可されるケースがあります。
目次に戻る
システムインターフェース
このセクションではここまで取り上げたもの以外でOSとのやり取りをするシステムインターフェースについて説明します。コマンドラインインターフェース
まずはシェルとのインターフェースです。その中でもコマンドライン引数は最も基本的なものでしょう。 rarakuコマンドでコマンドライン引数を指定するには -argsオプションを指定し、その後に引数を並べます。 例えば以下のようになります。
raraku args.rrks -args AA BB CC DD E
Rarakuコード上でコマンドライン引数を取得するには、Rrk_getCmdLineArgs() 関数を使います。 これはRarakuにデフォルトで定義されているネイティブ関数です。 importせずとも使うことができます。 この関数の戻り値の型はconststr[]であり、文字列の配列です。
例えば以下のようにして使用します。
varia args conststr[] = Rrk_getCmdLineArgs()
for( idx:=0; idx<Rrk_arynum(args); ++idx ){
Rrk_print( "arg[" idx "]=[" args[idx] "]\n" )
}
シェルに戻り値を返すにはグローバルスコープでreturn文を実行します。 以下のようにするとシェル側に値 0 が返ります。
return 0
0 の替わりにRrk_EXIT_SUCCESSと書くこともできます(Rrk_EXIT_SUCCESSは整数値0で定義された定数です)。
return Rrk_EXIT_SUCCESS
以下のようにするとシェル側に値 1 が返ります。
return 1
1 の替わりにRrk_EXIT_FAILUREと書くこともできます(Rrk_EXIT_FAILUREは整数値1で定義された定数です)。
return Rrk_EXIT_FAILURE
グローバルスコープでdefer文を使っており、尚且つ途中でreturn文を実行した場合、
関数内でそれを行った場合と同様に機能します。
以下をご覧ください。
この例の実行結果は以下のようになります。
defer Rrk_print( "at the end of program\n" )
Rrk_print( "global1\n" )
return Rrk_EXIT_FAILURE
Rrk_print( "global2\n" )
この例の実行結果は以下のようになります。
global1
at the end of program
マルチスレッドインターフェース
まず用語を定義しておきます。 このドキュメントではスレッドの呼び出し側を親スレッド、親スレッドから呼ばれるスレッドを子スレッドと呼ぶものとします。 また一番の祖先となる親スレッドを特にメインスレッドと呼ぶこともあります。 メインスレッドはプログラム実行時にシングルスレッドの状態で最初から実行されているスレッドそのものです。
Rarakuでマルチスレッドを実現するには、親スレッド側でRrkThread_create 関数を使います。 これはRarakuにデフォルトで定義されているネイティブ関数です。 この関数はthread.rrkhに宣言されており、使用にあたってはこれを事前にimportする必要があります。 この関数の引数は子スレッドとして呼び出す関数の関数名を指定します。 この関数の戻り値の型はRrkThread型のネイティブオブジェクトであり、親スレッドは通常これを変数として保持しておく必要があります。 この変数は子スレッドの終了を親スレッド側で待つ場合(RrkThread_wait関数の呼び出し)などに使用します。 まずこれを使用した一番簡単なコードを示します。
import std/thread
function child_func(){
Rrk_print( "child thread is called."\n )
}
varia thd = RrkThread_create( "child_func" )
Rrk_print( "main thread is running."\n )
RrkThread_wait( thd )
Rrk_print( "main thread end."\n )
上記では子スレッドの処理の実態であるchild_funcの関数名をRrkThread_createで指定し、 親スレッド側はそのchild_func関数の終了をRrkThread_wait関数で待つものとなります。 実行結果は以下のようになります。
main thread is running.
child thread is called.
main thread end.
マルチスレッドとしては何の意義もないほどシンプルな例ですが、一応child_funcが呼び出され、 その終了後にRrkThread_wait関数の呼び出し以降の処理が実行されていることが確認できます。
上記の実行結果で、"main thread is running." と "child thread is called." が出力されている行は同時に実行されているため、
これらが表示される順番が逆になる可能性もあります。
ただしchild_func関数が実際に子スレッドとして実行されるまでは少し時間がかかるため、
この例の場合、通常は"main thread is running" が先に実行されるとは思います。
またRrk_print関数はアトミックに実行されます。 つまり "main thread is running." の表示中に "child thread is called." という文字が割り込まれて "main thread child thread is called. is running." といった表示になる可能性はないということです。
またRrk_print関数はアトミックに実行されます。 つまり "main thread is running." の表示中に "child thread is called." という文字が割り込まれて "main thread child thread is called. is running." といった表示になる可能性はないということです。
さて、このままですと親スレッドと子スレッドでデータのやり取りができませんので、ここで何らかの共有データを持つ必要があります。 おそらく最初に思いつくであろうコードは、以下のようにグローバルスコープにスタティック変数を宣言することです (ただしこのコードは(意図的にこうしたのではない限り)問題があるコードです)。
import std/thread
static varia st_ival = 0
function child_func(){
Rrk_print( "child thread begin : " \=st_ival \n )
++st_ival; /* but this change IS NOT reflected in the main thread! */
Rrk_print( "child thread end : " \=st_ival \n )
}
st_ival = 10
Rrk_print( "main thread begin : " \=st_ival \n )
varia thd = RrkThread_create( "child_func" )
RrkThread_wait( thd )
Rrk_print( "main thread end : " \=st_ival \n )
上記の実行結果は以下のようになります。
main thread begin : st_ival=10
child thread begin : st_ival=10
child thread end : st_ival=11
main thread end : st_ival=10
確かに親スレッド側で "st_ival = 10" を実行した状態は子スレッド側にも反映され、子スレッドが開始した直前の時点ではこの値も10になっています。 しかしその後、子スレッドでst_ivalを1だけ増加させて11としていますが、その状態は親スレッド側には反映されていません(親スレッド側は依然として10のままです)。 なぜでしょうか?
実はRarakuのスタティック変数は(C言語などのように)スレッド間で共有されているわけではなく、子スレッドの起動時にそれらの完全なクローンが子スレッド側の環境に作られます。 よって上記の例では子スレッド側に(親スレッドとは別の)もう一つのst_ivalがクローンとして作られることになります。 そのため、子スレッド側のst_ivalは親スレッド側のst_ivalと始めは同じ値になっていますが、子スレッド側のst_ivalの値を変更したところで親スレッド側のst_ivalには何の変化もないことになります。
Rarakuにおける子スレッドは、まずOSレベルで(真の意味で)子スレッドを起動させ、そこでもう一つのRarakuVM(Raraku仮想マシン)を起動させることで実現されています。
この時、もう一つのRarakuVMでは、基本的に全てのスタティック変数について親スレッドと同じクローンを生成し、親スレッドと同じ環境を再現します
(ただし後述するようにRaraku標準で提供されるネイティブオブジェクトだけは、スタティック変数をスレッド間で共有することができます)。
その意味では、これはマルチスレッドではなくむしろマルチプロセスに近い概念ですが、OSレベルではマルチスレッドとなってはいるため、
Rarakuでもそう呼んでいます。
全てのスタティック変数についてクローンを生成するということは、スタティック変数の数があまりに多いと、 それだけ子スレッドの起動まで時間がかかることも意味します。
全てのスタティック変数についてクローンを生成するということは、スタティック変数の数があまりに多いと、 それだけ子スレッドの起動まで時間がかかることも意味します。
では子スレッド側で行った変更を親スレッド側へ反映させるにはどうすればよいでしょうか? Rarakuでは、例外的にネイティブオブジェクトについてはスタティック変数であってもスレッド間で共有することが可能となっています。 そこで以下のように専用のネイティブオブジェクト(RrkSharedUInt)を利用してこれを実現します。
import std/thread
import std/shared
static varia st_shdu RrkSharedUInt = RrkSharedUInt_create( 0 )
function child_func(){
varia uval = RrkSharedUInt_get( st_shdu )
Rrk_print( "child thread begin : " \=uval \n )
++uval;
Rrk_print( "child thread end : " \=uval \n )
RrkSharedUInt_set( st_shdu, uval ) /* update */
}
RrkSharedUInt_set( st_shdu, 10 )
Rrk_print( "main thread begin : uval=" RrkSharedUInt_get( st_shdu ) \n )
varia thd = RrkThread_create( "child_func" )
RrkThread_wait( thd )
Rrk_print( "main thread end : uval=" RrkSharedUInt_get( st_shdu ) \n )
上記では、まずRrkSharedUIntが宣言されたshared.rrkhをimportする必要があります。 次にRrkSharedUInt_create関数でRrkSharedUInt型のネイティブオブジェクトを作ります (今回はわかりやすさのためst_shduの直後にRrkSharedUInt型を明示的に指定していますが、勿論この指定は省略することも可能です)。
st_shduはuint型をラップしたネイティブオブジェクトとなっています。 そのためこれに直接uint型を代入することはできず、RrkSharedUInt_set関数で値を設定する必要があります。 また逆にuint型の値の取り出しはRrkSharedUInt_get関数を使います。
子スレッド側ではRrkSharedUInt_get関数によって内部の値を取り出して一旦uval変数に格納し、それをインクリメントした後、 RrkSharedUInt_set関数によってst_shdu内の値を更新しています。この瞬間、親スレッド側のst_shduの値も更新されることになります。
実行結果は以下のようになります(今度は期待通りの結果です)。
main thread begin : uval=10
child thread begin : uval=10
child thread end : uval=11
main thread end : uval=11
スレッド間で共有できるネイティブオブジェクトとしては、他にもRrkSharedStr(文字列共有用)、
RrkSharedBfr(uint[]、つまり任意のバイト列共有用)などがあり、これらはshared.rrkhに宣言されています。
またRrkFile、RrkRgx、RrkBirdなどのその他のネイティブオブジェクトもスレッド間で共有可能です。
マルチスレッドプログラミングにおいては、lock/unlockを掛けるためのmutexが必要になる場合もあります。 RarakuではこのようなmutexとしてRrkMutexが用意されており、mutex.rrkhで宣言されています。 RrkMutexもまたネイティブオブジェクトであるため、(mutexである以上当然ではありますが)スレッド間での共有が可能です。 以下の例をご覧下さい。
import std/thread
import std/shared
import std/mutex
varia st_counter = RrkSharedUInt_create( 0 )
varia st_shds = RrkSharedStr_create( "none" )
varia st_mtx = RrkMutex_create()
function child_func()
{
RrkMutex_lock( st_mtx ){
varia u = RrkSharedUInt_get( st_counter )
++u;
RrkSharedUInt_set( st_counter, u )
}RrkMutex_unlock( st_mtx )
RrkMutex_lock( st_mtx ){
varia str = RrkSharedStr_getEx( st_shds, 0, Rrk_NPOS ) /* str is temporary string */
str &= ":child"
RrkSharedStr_setEx( st_shds, 0, str, 0, Rrk_NPOS )
}RrkMutex_unlock( st_mtx )
}
varia thd = RrkThread_create( "child_func" )
RrkMutex_lock( st_mtx ){
varia u = RrkSharedUInt_get( st_counter )
++u;
RrkSharedUInt_set( st_counter, u )
}RrkMutex_unlock( st_mtx )
RrkMutex_lock( st_mtx ){
varia str = RrkSharedStr_getEx( st_shds, 0, Rrk_NPOS ) /* str is temporary string */
str &= ":main"
RrkSharedStr_setEx( st_shds, 0, str, 0, Rrk_NPOS )
}RrkMutex_unlock( st_mtx )
RrkThread_wait( thd )
Rrk_print( "st_counter=" RrkSharedUInt_get(st_counter) \n )
Rrk_print( "st_shds=" RrkSharedStr_getEx(st_shds, 0 ,Rrk_NPOS) \n )
上記ではst_counterとst_shdsがそれぞれスレッド間での共有データ(RrkSharedUInt、RrkSharedStr)となっており、 親スレッドと子スレッドそれぞれにおいて、st_counterについては値を1インクリメント、とst_shdsについては文字列の連結を行っています。 これらの処理はいずれもまずget関数で値を一時変数に格納してその値を変え、最後にset関数にその値を与えて更新するという3ステップからなります。 またこれらの3ステップはアトミックに行う必要があります。
もしもこれらがアトミックでなかった場合(すなわち互いに割り込むようなタイミングで実行する可能性を許す場合)、
例えば次のような順番で処理が進む可能性があり、意図通りの結果とはならない恐れが生じます。
仮に上記の順番で実行された場合、最終的に親スレッドでRrkThread_waitが終了した時点でのst_counterの値は1となります。 期待される結果はst_counter=2と思われますのでこれは問題となります。
st_shdsについても同様で、実行タイミングによっては最終的なst_shdsの値は"none:child" あるいは "none:main" となる可能性があります。 期待される結果は "none:main:child" あるいは "none:child:main" と思われますのでやはり問題となります。
varia u = RrkSharedUInt_get( st_counter ) /* 親スレッド(u=0) */
varia u = RrkSharedUInt_get( st_counter ) /* 子スレッド(u=0) */
++u; /* 親スレッド(u=1) */
++u; /* 子スレッド(u=1) */
RrkSharedUInt_set( st_counter, u ) /* 親スレッド(st_counter=1) */
RrkSharedUInt_set( st_counter, u ) /* 子スレッド(st_counter=1) */
仮に上記の順番で実行された場合、最終的に親スレッドでRrkThread_waitが終了した時点でのst_counterの値は1となります。 期待される結果はst_counter=2と思われますのでこれは問題となります。
st_shdsについても同様で、実行タイミングによっては最終的なst_shdsの値は"none:child" あるいは "none:main" となる可能性があります。 期待される結果は "none:main:child" あるいは "none:child:main" と思われますのでやはり問題となります。
共有データXにアクセスを伴う一連の処理をアトミックに行うには、あるスレッドAがXにアクセスする処理を実行中であり、 しかもそれ以外のスレッドBもまた同時にXにアクセスするような状況が生じる可能性がある場合、 BはAの(この部分の)実行が終了するまで一旦待つ必要があります。 このようなAとBの処理を連絡付けする役割を持つのがst_mtx(RrkMutex)であり、 アトミックで行う必要がある処理をRrkMutex_lock( st_mtx ) と RrkMutex_unlock( st_mtx )で囲います。 st_mtxはRrkMutex_create関数で予め作っておきます。
上記の例ではRrkMutex_lockとRrkMutex_unlockの間に「{}と「}」を入れてインデントしていますが、
これは別になくても構いません(単にコードの見易さとテキストエディタの自動インデントを利用する関係で入れているだけです)。
尚、ここでdefer文を使うことは基本的にはできません。 defer文の場合、関数終了時に指定された関数が実行されるためです。 RrkMutex_unlockはアトミックなアクセスが終了したタイミングでただちに呼び出さなければなりません。 特に1つの関数で複数のRrkMutex_unlockがあった場合にdefer文を使ってしまうと、結果的にlock/unlockがネストされたのと同じような状況になり、 デッドロックが発生します。
尚、ここでdefer文を使うことは基本的にはできません。 defer文の場合、関数終了時に指定された関数が実行されるためです。 RrkMutex_unlockはアトミックなアクセスが終了したタイミングでただちに呼び出さなければなりません。 特に1つの関数で複数のRrkMutex_unlockがあった場合にdefer文を使ってしまうと、結果的にlock/unlockがネストされたのと同じような状況になり、 デッドロックが発生します。
RrkMutexは複数作ることもできますが、スレッドAとBで同時実行された場合において、同じRrkMutexにおいてlock/unlockされている場合のみ、
その部分がアトミックとなり「待ち」が発生する可能性があります。
逆に言えば異なるRrkMutexでlock/unlockされていた場合、その部分はアトミックとはならず「待ち」も発生しません。
よってスレッドAとBが同じ共有データXにアクセスを行う場合、RrkMutexもまたその部分で同じものにしておく必要があります。 同じRrkMutexにすべき部分で異なるRrkMutexにしてしまうと結局その部分がアトミックとはならず問題が生じます。
面倒であればすべての箇所で同じRrkMutexを使いまわしても構いませんが(実際、上記の例でもそうしていますが)、 処理の組み合わせによっては若干無駄な「待ち」が発生する可能性はあります。 例えば、XとYは互いに完全に独立した共有データであるとして、 スレッドAがXにアクセス中でスレッドBがYにアクセス中といった明らかに問題の生じないタイミングにおいては、 MとNを異なるRrkMutexにすることによってそのような無駄な「待ち」の発生を防止することができる場合もあります。 上記の例の場合でその点に拘りたいなら、まずst_mtx1とst_mtx2を作り、st_counterにアクセスする部分でのlock/unlockではst_mtx1を、 st_shdsにアクセスする部分でのlock/unlockではst_mtx2を指定するなどのようにすればよいでしょう。
よってスレッドAとBが同じ共有データXにアクセスを行う場合、RrkMutexもまたその部分で同じものにしておく必要があります。 同じRrkMutexにすべき部分で異なるRrkMutexにしてしまうと結局その部分がアトミックとはならず問題が生じます。
面倒であればすべての箇所で同じRrkMutexを使いまわしても構いませんが(実際、上記の例でもそうしていますが)、 処理の組み合わせによっては若干無駄な「待ち」が発生する可能性はあります。 例えば、XとYは互いに完全に独立した共有データであるとして、 スレッドAがXにアクセス中でスレッドBがYにアクセス中といった明らかに問題の生じないタイミングにおいては、 MとNを異なるRrkMutexにすることによってそのような無駄な「待ち」の発生を防止することができる場合もあります。 上記の例の場合でその点に拘りたいなら、まずst_mtx1とst_mtx2を作り、st_counterにアクセスする部分でのlock/unlockではst_mtx1を、 st_shdsにアクセスする部分でのlock/unlockではst_mtx2を指定するなどのようにすればよいでしょう。
目次に戻る
goto文とラベル文
goto文は指定したラベル文字列と同じものを持つラベル文へ一気にジャンプします (以下ではこのようなgoto文とラベル文を「呼応する」と表現します)。gotoの後ろにスペース区切りでジャンプ先のラベル文字列を指定します。 ラベル文には、同じラベル文字列を書き、その後ろに「:」を指定します。 例えば以下のようになります。
goto Destination /* goto文 */
…
Destination: /* ラベル文 */
gotoはgoとtoを合体させたようなキーワードになっています。
他の言語でも登場するキーワードですが、これはさすがに「ゴートゥ」と発音しましょう
(「ゴトー」ではなく)。
C言語のgoto文、ラベル文をご存知の方は、使い方はほぼそれと同じです。
goto文の使い方として最も定石的なものは深いブロックのネストからの脱出や、 エラーハンドリングの際に関数の終わりへ一気にジャンプしたいような場合などですが、 Rarakuでもこのような使用が可能です。 ただしRarakuでは望ましくない副作用が発生し得るジャンプは禁止されています。 例えばブロックの中へのジャンプや、変数が未初期化のままになり得るようなジャンプは禁止されており、 そのような記述をしてもコンパイルエラーとなります。
goto文の使い方として最も定石的なものは深いブロックのネストからの脱出や、 エラーハンドリングの際に関数の終わりへ一気にジャンプしたいような場合などですが、 Rarakuでもこのような使用が可能です。 ただしRarakuでは望ましくない副作用が発生し得るジャンプは禁止されています。 例えばブロックの中へのジャンプや、変数が未初期化のままになり得るようなジャンプは禁止されており、 そのような記述をしてもコンパイルエラーとなります。
戻る方向へジャンプすることも可能です。 例えば以下のような記述はOKです。
BEGIN:
…
goto BEGIN
goto文は関数の中およびグローバルスコープのいずれでも使用できます。 ただしgoto文が関数内にある場合、呼応するジャンプ先のラベル文はそのgoto文と同じ関数内になければなりません。 またgoto文がグローバルスコープにある場合は、呼応するジャンプ先のラベル文はグローバルスコープになければなりません。
ブロックを伴うケース
途中にブロックを伴うケース(これはfor文やwhile文、if文などのブロックも含まれます)では、いくつかの注意点があります。 まず、同一ブロック内へのジャンプは勿論可能です。 このとき呼応するgotoとラベル文の間に他のブロックや他のgotoとラベル文があっても構いません。 例えば以下のような記述はOKです。
goto GlobalScope
{
goto SameBlodk
…
SameBlodk: /* OK */
}
GlobalScope: /* OK */
Rarakuでは基本的にブロックの中へ入る向きにジャンプすることは禁止されています。
ここはC言語と大きく異なる点です。
例えば以下のような記述はコンパイルエラーとなります。
goto IntoBlock
{
IntoBlock: /* NG! */
}
ブロックの外へ出る方向へのジャンプは可能です。 例えば以下のような記述はOKです。
{
goto OutOfBlock
}
OutOfBlock: /* OK */
一度ブロックを出て、他のブロック内に入るといったジャンプも禁止されています。 例えば以下のような記述はコンパイルエラーとなります。
{
goto OtherBlock
}
{
OtherBlock: /* NG */
}
複数のgoto文や同じラベル文字列があるケース
呼応するgoto文とラベル文は必ず一対であるとは限りません。 例えば以下のように複数のgoto文が一つのラベル文にジャンプするのは問題ありません。
goto END
…
goto END
{
goto END
}
END:
しかしこの逆、すなわち同じラベル文字列のラベル文が複数あってはいけません。 例えば以下はコンパイルエラーとなります。
goto END
…
END:
…
END: /* NG: ENDが複数ある */
異なる関数やグローバルスコープにおいて、同じラベル文字列を使用するのは問題ありません。 例えば以下ではすべてENDというラベル文字列を使っていますが、それぞれ関数やグローバルスコープが異なり、 それらの中においてのみその呼応が機能します。
function my_func1() void
{
/* OK */
goto END
…
END:
}
function my_func2() void
{
/* OK */
goto END
…
END:
}
/* OK */
goto END
…
END:
呼応するgotoとラベル文の間にvaria/const/tight/refer文があるケース
Rarakuでは呼応するgotoとラベル文の間にvaria/const/tight/refer文があり、しかもそれらがラベル文と同じブロックにある場合、コンパイルエラーとなります。 例えば以下のような記述はいずれもNGです。
goto END
varia i int = 9 /* NG: 呼応するgotoとラベル文の間にあるvaria文(ラベル文と同じブロック) */
END:
BEGIN:
varia i int = 9 /* NG: 呼応するgotoとラベル文の間にあるvaria文(ラベル文と同じブロック) */
goto BEGIN
{
goto END
}
varia i int = 9 /* NG: 呼応するgotoとラベル文の間にあるvaria文(ラベル文と同じブロック) */
END:
しかし呼応するgotoとラベル文の間にvaria/const/tight/refer文があっても、それらがラベル文と異なるブロックにある場合は問題ありません。 例えば以下のような記述はいずれもOKです。
{
goto END
varia i int = 9 /* OK: 呼応するgotoとラベル文の間にあるが、そのラベル文とはブロックが異なるvaria文 */
}
END:
goto END
{
varia i int = 9 /* OK: 呼応するgotoとラベル文の間にあるが、そのラベル文とはブロックが異なるvaria文 */
}
END:
{
{
goto END
}
varia i int = 9 /* OK: 呼応するgotoとラベル文の間にあるが、そのラベル文とはブロックが異なるvaria文 */
}
END:
呼応するgotoとラベル文の間にその他の特殊なものがあるケース
グローバルスコープにおいて、gotoとラベル文の間に関数定義があるのは問題ありません。 例えば以下のような記述はOKです。
goto END
…
function my_func() void
{
…
}
…
END:
一方、gotoとラベル文の間にstatic関数またはglobal関数の定義があり、 しかもそれらの関数がグローバルブロックの配下にあるような状況は禁止されています。 例えば以下のような記述はコンパイルエラーとなります。
{
{
goto L
}
{
/***
* NG: gotoとラベル文の間にstatic関数またはglobal関数の定義があり、
* しかもその定義がグローバルブロックの配下にあるような状況.
*/
static static_func()
{
}
}
}
L:
ただし同じようなブロック構造でも、定義されているものがローカル関数や無名関数やlambda式やlambda関数なら問題ありません。 例えば以下のような記述はOKです。
{
{
goto L
}
{
/***
* OK: gotoとラベル文の間にローカル関数の定義があり、
* しかもその定義がグローバルブロックの配下にあるような状況.
*/
function local_func()
{
}
}
}
L:
このような仕様になっているのは、
以下のようにstatic変数が絡む状況で問題があるためです。
上記の状況が仮に許可されると、goto文が実行された後にラベルLに至りますが、 定義されているものはstatic関数であるため、この位置からでも呼び出しが可能となります。 しかしstatic変数st_valの初期化は行われていないままです。 つまりこのままではstatic_func関数内を経由して未初期化の状態のst_valにアクセスすることになってしまいます。 これは防がなければなりません。 そのため、このような状況をコンパイルエラーとする仕様にしてあります。
一方、定義されているものがローカル関数等である場合は、このような制限を設けなくても別のガードが働くなどして (未初期化の変数へアクセスするような事態に対して)うまくコンパイルエラーが発動します。 その理由は以下の通りです。
{
{
goto L
}
static varia st_val=10
{
static static_func()
{
Rrk_print( \=st_val\n )
}
}
}
L:
static_func()
上記の状況が仮に許可されると、goto文が実行された後にラベルLに至りますが、 定義されているものはstatic関数であるため、この位置からでも呼び出しが可能となります。 しかしstatic変数st_valの初期化は行われていないままです。 つまりこのままではstatic_func関数内を経由して未初期化の状態のst_valにアクセスすることになってしまいます。 これは防がなければなりません。 そのため、このような状況をコンパイルエラーとする仕様にしてあります。
一方、定義されているものがローカル関数等である場合は、このような制限を設けなくても別のガードが働くなどして (未初期化の変数へアクセスするような事態に対して)うまくコンパイルエラーが発動します。 その理由は以下の通りです。
- ローカル関数やlambda関数の定義である場合
- 無名関数やlambda式の定義である場合
まず、関数定義が上記のような構造下にあってラベルLがそれより上層のブロックにある場合、 goto文を実行して上層のラベルLに到った時点で「ローカル関数を呼び出し可能なブロック範囲」が終了します。 つまり、そこからローカル関数を呼ぶことはもう不可能になります。
また、ラベルLが関数定義と同じブロック階層にあった場合、ローカル関数の呼び出しは可能ですが、 今度は「gotoとラベルの間にあり、かつそのラベル文と同じ階層にあるvaria文は宣言できない」規則によりガードされ、 st_valのような変数の宣言は許可されません。
最後に、ラベルLが関数定義より下の階層にあった場合、(ブロック階層を下がるか跨るかのジャンプになるため)そもそもgotoの実行が許可されません。
よって、いずれにしても未初期化の変数st_valにアクセスするような事態にはならないか、コンパイルエラーで防止されることになります。
まず、関数定義が上記のような構造下にあってラベルLがそれより上層のブロックにある場合、 goto文を実行して上層のラベルLに到った時点で「これらの関数型変数が宣言されたブロック範囲」が終了します。 つまり、そこから関数型変数を介した呼び出しはもう不可能になります。
これより上の階層に予め別のstaticな関数型変数を用意しておき、それに代入して後から間接的に呼び出すような状況も想定できますが、 しかしそもそも、これらの関数型変数をそれに代入するための代入文は、このような状況の場合、必ずgotoとラベル文の間になければならないはずです (無名関数やlambda式の定義の位置よりそれが後でなければ、代入元の実体はまだ存在しませんし、 また定義を参照可能なブロックを抜ける前にその代入は行わなければ代入元を参照できなくなります)。 今、その区間をジャンプしてスキップするのですから、そのような代入文の実行は不可能になります。
また、ラベルLが関数定義と同じブロック階層にあった場合、無名関数やlambda式の呼び出しは可能ですが、 今度は「gotoとラベルの間にあり、かつそのラベル文と同じ階層にあるvaria文は宣言できない」規則によりガードされ、 st_valのような変数の宣言は許可されません。
最後に、ラベルLが関数定義より下の階層にあった場合、(ブロック階層を下がるか跨るかのジャンプになるため)そもそもgotoの実行が許可されません。
よって、いずれにしても未初期化の変数st_valにアクセスするような事態にはならないか、コンパイルエラーで防止されることになります。
gotoとラベル文の間にstatic関数またはglobal関数の定義があり、 しかもそれらの関数が(グローバルブロック配下ではなく)グローバルスコープにある場合であれば問題ありません。 例えば以下のような記述はOKです。
{
goto L
}
/***
* OK: gotoとラベル文の間にstatic関数またはglobal関数の定義があり、
* しかもその定義がグローバルスコープにあるような状況.
*/
static static_func()
{
}
L:
グローバルスコープではfunctionキーワードを用いて定義してもstatic関数になりますから、 これは結局最初に述べた規則と同じです。
これが問題ない理由も見ておきます。
先ほどと同じく以下のようにstatic変数が絡む状況を考えます。
これもまた、別の規則も合わさってうまくコンパイルエラーになります。
static_funcはグローバルスコープにあるのですから、そこから参照されるst_valを宣言するならそれは必ずグローバルスコープになければなりません。 そして、goto文でグローバルスコープにあるstatic_funcを飛び越えるということは、少なくともラベルLの方もグローバルスコープになければなりません (さもなければブロック階層を下がるか、ブロックを跨ぐようなジャンプになりますが、それは許可されません)。 従ってst_valとラベルLの階層は一致し、「gotoとラベルの間にあり、かつそのラベル文と同じ階層にあるvaria文は宣言できない」規則によりガードされ、 st_valのような変数の宣言は許可されません。
よって、未初期化の変数st_valにアクセスするような事態にはならないか、コンパイルエラーで防止されることになります。
{
goto L
}
static varia st_val=10
static static_func()
{
Rrk_print( \=st_val\n )
}
L:
static_func()
static_funcはグローバルスコープにあるのですから、そこから参照されるst_valを宣言するならそれは必ずグローバルスコープになければなりません。 そして、goto文でグローバルスコープにあるstatic_funcを飛び越えるということは、少なくともラベルLの方もグローバルスコープになければなりません (さもなければブロック階層を下がるか、ブロックを跨ぐようなジャンプになりますが、それは許可されません)。 従ってst_valとラベルLの階層は一致し、「gotoとラベルの間にあり、かつそのラベル文と同じ階層にあるvaria文は宣言できない」規則によりガードされ、 st_valのような変数の宣言は許可されません。
よって、未初期化の変数st_valにアクセスするような事態にはならないか、コンパイルエラーで防止されることになります。
Rarakuにはtry文と呼ばれるものがあり、if文と同様のブロックを作りますが、 そのブロックの内部にgotoがあった場合、そのブロックの外部にあるラベル文に向けてジャンプすることはできません。 このあたりについての詳細は、「大域脱出とtry文」のセクションで詳しく説明します。
目次に戻る
モジュールとトレイト
結合の複雑さを軽減する
「ヘッダファイルとリンケージ」のセクションではプログラムを複数のファイルに分ける方法について述べました。 プログラムの規模が大きくなってくると一つのファイルにすべてを記述しては管理が困難になります。 全体を意味あるグループに分割し、それらを別ファイルとすべきです。 このようにある一つの意味でまとまった一つのファイルをモジュールと呼びます。
モジュールは他のファイルから使われますが、その中身がどのような実装になっているかという情報は不要です。 通常、モジュールの実装は外部からアクセスできないように遮断または隠蔽します。 これを情報隠蔽と呼びます。
外部へは窓口となるインターフェースのみを公開します。 このようにすることで、プログラム全体とそのモジュールが結合する部分をそのインターフェース部分のみに限定することができ、 結果的に結合の複雑さを軽減することができます。 これを結合を疎にする(loose coupling)と呼びます。
不完全型構造体
では具体的にどのようにすれば実装を隠蔽し、結合を疎にできるのでしょうか? これには様々な方法がありますが、まずはC言語などで伝統的に行われている関数と構造体を用いた方法について紹介します (Rarakuでもこれは可能です)。
不完全型構造体を使ったモジュールの情報隠蔽は、C言語などでは伝統的に行われてきた手法であるため、
例えばC言語のソースコードを取り急ぎRarakuのコードに移植したいような場合は便利かと思います。
そのためここでもまず最初に取上げますが、一方でこの手法はヘッダやファイルスコープをややアクロバティックに使うことを前提としている上、
注意事項も多いためC言語に慣れていない方にとっては扱いが難しいかもしれません。
ややこしいようならこの不完全型構造体による手法はスキップして次の項へ進んでも構いません。
構造体の話に深入りする前にまず関数で同様のことをするにはどうすればよかったかをおさらいしましょう。 「ヘッダファイルとリンケージ」のセクションでも述べましたが、 関数のインターフェース部分だけを外部に公開するには rrksでglobal関数を定義し、rrkhでそのプロトタイプ宣言を記述します。 外部からはそのヘッダファイルをインポートすることで、その関数が呼び出せるようになります。 このとき、関数の実装はそれが定義されているファイル(rrksファイル)内のみに閉じ込められ、 つまり外部との結合が疎になっていることになります。
今、構造体のメンバでこれと同じことを実現したいとします。 ある一つの構造体を見たとき、その中には外部へ公開する必要があるメンバもあれば、 その必要のない(モジュールの内部実装においてのみアクセスされる)メンバもあることでしょう。 前者をパブリックメンバ、後者をプライベートメンバと呼びます。
一般的には構造体のすべてのメンバがプライベートメンバである場合も多いです。
あるいはそれらへアクセスする唯一の手段としてアクセサ関数が用意されることもよくあります。
このとき構造体の定義(つまりstruct文ですが)を単にヘッダに書いてしまうと、 これをインポートした側で、構造体のメンバすべてがまる見えになってしまいます。 つまりすべてがパブリックメンバとして扱われてしまい、どれ一つとしてプライベートメンバとすることができません。
Rarakuでは、 「struct 構造体名 -> 実体を定義した構造体名 { パブリックメンバの羅列 }」といった書式により、 構造体の宣言(実体の定義ではなく)にあたるものを記述することが可能です。 結論から言えばこの記述により、「パブリックメンバの羅列」とあるメンバのみを外部に公開し、 それ以外のメンバをプライベートメンバにすることが可能になります。 Rarakuにおいてこのような構造体には特別な名前がついており、不完全型構造体と呼びます。
C言語にも同様のものがあり、この「不完全型構造体」という名前もC言語から拝借したものです。
不完全型構造体はその構造体の定義そのものではなく、あくまで補助的な宣言であり、 通常これはヘッダファイル(rrkh)の方に記述します。 一方、その実体をを定義した構造体は、実装ファイル(rrks)の中に記述します。 この実体を定義した構造体のことを不完全型構造体と対比する文脈において完全型構造体と呼ぶこともあります。
不完全型構造体に対応する完全型構造体の名前は、上記の書式において「->」の直後に指定します。 例えば以下のようになります。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{
i int /* パブリックメンバ */
}
global MyStruct_create() MyStruct
global MyStruct_print() void
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct { /* 必ず親として呼応する不完全構造体を指定 */
i int
j int
k int
}
global MyStruct_create() MyStruct {
/* 完全型構造体を基に変数sを宣言してそれを確保 */
varia si MyStructImpl = {}
return si
}
global MyStruct_print( s MyStruct ) void {
const si = s->MyStructImpl /* ^downcastではない点に注意 */
Rrk_print( \=si.i \n ) /* OK */
Rrk_print( \=si.j \n ) /* OK */
Rrk_print( \=si.k \n ) /* OK */
}
上記では、my_util.rrks内でmy_util.rrkhをインポートしています。 my_util.rrkh内には不完全型構造体のMyStructがあり、それに対応する完全構造体としてMyStructImplが指定されています。 my_util.rrks内にはMyStructに対応する完全型構造体MyStructImplが定義されており、 この親としては必ず対応する不完全構造体(この例の場合MyStruct)を指定しなければなりません。 またこのmy_util.rrks内では、MyStructImplを生成するためのMyStruct_create関数(ただし戻り値の型はMyStruct)と その内容を出力するためのMyStruct_print関数が定義されています。
MyStruct_create関数内の「varia si MyStructImpl = { }」では、完全型構造体を基にその実体を確保します。 これを外部へはMyStruct型として返しています。
MyStruct_print関数では引数としてMyStruct型のsを受け取り、 それをMyStructImpl型へキャストしてsiに代入しています。 このsiはMyStructImplですから、そこに定義されたすべてのメンバ(i、j、k)にアクセス可能です。
ここで注意して頂きたいのは、MyStructImplの親としてMyStructが指定されているということです。 つまりここで行っているキャストは、親であるMyStructからその子であるMyStructImplのキャストです。
「型のキャストと暗黙変換」のセクションでも述べましたが、親の構造体から子の構造体へキャストする場合、 通常はスタティックキャスト演算子「->」ではなくダウンキャスト演算子「^downcast」を使う必要があります。 それにも関わらず、上記では奇妙なことにこのキャストが(^downcastではなく)「->」で実現できています。 その理由が気になる方は下記の参考をお読みください。 基本的には不完全構造体から完全構造体へのキャストは特別に「->」で実現できると記憶してもらえば十分です。
参考: 不完全構造体から完全構造体へのキャストが「->」で実現できる理由
これが可能な理由は二つあります。
一つ目は不完全構造体MyStructで完全構造体MyStructImplを指定した場合、MyStructの子になれる構造体はMyStructImplに限定されるということです。 つまりその他の構造体をMyStructの子とすることは一切許可されなくなります。 またその逆に、完全型構造体MyStructImplの定義では、その親として必ずMyStructを指定しなければなりません。 これによって、この二つに1:1の関係が形成されます。
二つ目はMyStructの生成にあたっては必ず上記のMyStruct_createを使う必要があるということです。 これを使わず、MyStruct単独では構造体を生成することはできません。 例えば以下はコンパイルエラーとなります。
上記では構造体の初期化子「{ }」を使って不完全構造体の実体を確保しようとしていますが、 これは許可されません。その他、関数のMyStruct型引数に「{ }」を指定したり、 戻り値の型がMyStruct型の関数内でreturn文に「{ }」を指定したり、 構造体のメンバを「{ }」で初期化することも不完全型構造体においては許可されません。
これにより、不完全構造体MyStructの実体は必ずMyStructImplとなり、その他の構造体がその実体である可能性を排除できます。 従ってダウンキャスト演算子を使ってMyStruct型からMyStructImpl型へ変換するまでもなく このキャストが成功することはコンパイル段階で静的に明らかです。 そのためこのキャストは(^downcastではなく)「->」で実現できるというわけです。
Close
一つ目は不完全構造体MyStructで完全構造体MyStructImplを指定した場合、MyStructの子になれる構造体はMyStructImplに限定されるということです。 つまりその他の構造体をMyStructの子とすることは一切許可されなくなります。 またその逆に、完全型構造体MyStructImplの定義では、その親として必ずMyStructを指定しなければなりません。 これによって、この二つに1:1の関係が形成されます。
二つ目はMyStructの生成にあたっては必ず上記のMyStruct_createを使う必要があるということです。 これを使わず、MyStruct単独では構造体を生成することはできません。 例えば以下はコンパイルエラーとなります。
struct MyStruct->MyStructImpl{
i int
}
varia s MyStruct = {} /* Compile error */
上記では構造体の初期化子「{ }」を使って不完全構造体の実体を確保しようとしていますが、 これは許可されません。その他、関数のMyStruct型引数に「{ }」を指定したり、 戻り値の型がMyStruct型の関数内でreturn文に「{ }」を指定したり、 構造体のメンバを「{ }」で初期化することも不完全型構造体においては許可されません。
これにより、不完全構造体MyStructの実体は必ずMyStructImplとなり、その他の構造体がその実体である可能性を排除できます。 従ってダウンキャスト演算子を使ってMyStruct型からMyStructImpl型へ変換するまでもなく このキャストが成功することはコンパイル段階で静的に明らかです。 そのためこのキャストは(^downcastではなく)「->」で実現できるというわけです。
Close
この不完全構造体から完全構造体へのキャストは、少し短く演算子「^impl」を使って書くこともできます。 例えば以下をご覧下さい。
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct { /* 必ず親として呼応する不完全構造体を指定 */
i int
j int
k int
}
global MyStruct_create() MyStruct {
/* 完全型構造体を基に変数sを宣言してそれを確保 */
varia si MyStructImpl = {}
return si
}
global MyStruct_print( s MyStruct ) void {
const si = s^impl /* const si = s->MyStructImpl と書いたのと同じ */
Rrk_print( \=si.i \n ) /* OK */
Rrk_print( \=si.j \n ) /* OK */
Rrk_print( \=si.k \n ) /* OK */
}
global MyStruct_getI( s MyStruct ) void {
return s^impl.i /** return s->MyStructImpl.i と書いたのと同じ */
}
演算子「^impl」はオペランドとして与えられた不完全構造体に対応する完全構造体の型を自動的に認識し、 その型へキャストしたものを返します。 実際の実装ではこの演算子「^impl」を使う方が便利でしょう。
次に、これらの外側について考えましょう。 以下のように別のファイルmain.rrksを作り、その中でmy_util.rrkhをインポートした場合、 main.rrks側には不完全型構造体MyStructの情報のみが現れることになります(MyStructImplの情報は隠蔽されます)。
main.rrks
import my_util /* ヘッダファイルの中に不完全型構造体MyStructがある */
varia s MyStruct = MyStruct_create()
s.i = 1 /* OK */
//s.j = 2 /* コンパイルエラー: メンバjは不完全型構造体MyStruct内に存在しない */
//s.k = 3 /* コンパイルエラー: メンバkは不完全型構造体MyStruct内に存在しない */
不完全型構造体MyStructの(struct文における)宣言内では、パブリックメンバ i のみが記述されています。 そのため、main.rrks内では、「s.i」といった(メンバ i への)アクセスのみが許可されます。 一方、その他メンバ j、k はパブリックメンバとしては記述されていませんので、これらにアクセスすることは(main.rrks内からは)できません。 つまりこれは、j、k が(他のファイルからはアクセスできない)プライベートメンバになっているということを意味します。 このようにして構造体における情報隠蔽が実現できることになります。
最後に注意しなければならないのは、不完全構造体内と完全構造体内におけるパブリックメンバの指定の仕方です。 不完全構造体内で指定するパブリックメンバの羅列は、必ず対応する完全型構造体内の一番初めのメンバから順番に、 しかも型と名前を一致させて記述しなければなりません。 例えば以下の通りです。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{
i int /* OK : iが1番目なので1番目に書く */
j int /* OK : jが2番目なので2番目に書く */
}
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct{
i int /* iが1番目 */
j int /* jが2番目 */
k int /* kが3番目 */
}
上記では、int型のメンバiが完全型構造体において1番目にあるので、不完全型構造体においてもこれを1番目に書きます。 int型のメンバjについても同様に完全型構造体において2番目にあるので、不完全型構造体においてもこれを2番目に書きます。 尚、不完全型構造体内に指定したメンバの個数が完全型構造体において指定したメンバの個数より少ないのは問題ありません。
もしもこの順番を崩してパブリックメンバを書いた場合はコンパイルエラーとなります。 例えば以下のような記述は許可されません。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{
j int /* コンパイルエラー: jは2番目なのに1番目に書いている */
i int /* コンパイルエラー: iは1番目なのに2番目に書いている */
}
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct{
i int /* iが1番目 */
j int /* jが2番目 */
k int /* kが3番目 */
}
上記では、完全型構造体におけるメンバ j は2番目に位置しているにも関わらず、不完全型構造体内でそれを1番目に記述しており、 コンパイルエラーとなります。
これが許可されない理由は、構造体のメンバのアクセスは最終的にインデックス表現を使ったRaraku仮想マシン命令に変換されるからです
(高速化のため、連想配列等は使っていません)。
つまり仮にこれを許可した場合、ソースコード上では「メンバ j にアクセス」しているつもりでも、
最終的な命令は「0 番目のメンバにアクセス」といった表現に変換されます。
これは結局、完全型構造体の側で言えば「メンバ i にアクセス」していることになりますから、
多分プログラマの意図と食い違う挙動でしょう。
そのため、このような記述をそもそも認めずコンパイルエラーとしてあります。
同様に以下のような記述もコンパイルエラーとなります。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{
i int /* OK : iは1番目なので1番目に書く */
k int /* コンパイルエラー: kは3番目なのに2番目に書いている */
}
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct{
i int /* iが1番目 */
j int /* jが2番目 */
k int /* kが3番目 */
}
上記では、完全型構造体におけるメンバ k は3番目に位置しているにも関わらず、不完全型構造体内でそれを2番目に記述しており、 コンパイルエラーとなります。 しかしながらこの例の場合、jをパブリックメンバにするわけにはいかないため不完全型構造体内においてkを2番目に書かざるを得ません。 この場合はもはや完全型構造体内のメンバkの順番を変える他ありません。 つまり以下のようにします。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{
i int /* OK : iは1番目なので1番目に書く */
k int /* OK : kは2番目なので2番目に書く */
}
my_util.rrks
utilize
import my_util
/* 完全型構造体(通常は実装ファイルの中に閉じ込めて書く) */
struct MyStructImpl : MyStruct{
i int /* iが1番目 */
k int /* kを2番目に変更 */
j int /* jを3番目に変更 */
}
すべてのメンバをプライベートメンバにしたい場合、不完全型構造体のstructのメンバを空にすればOKです。 例えばヘッダファイルでの宣言を以下のようにします。
my_util.rrkh
/* 不完全型構造体(通常はヘッダに書く) */
struct MyStruct->MyStructImpl{} /* パブリックメンバは空 */
global MyStruct_create() MyStruct
global MyStruct_print( s MyStruct ) void
逆にすべてのメンバをパブリックメンバにしたい場合、そもそも不完全型構造体を使う必要がありません。 以下のように単にヘッダファイルで通常の構造体を定義すればOKです。
my_util.rrkh
struct MyStruct{
i int
j int
k int
}
global MyStruct_create() MyStruct
global MyStruct_print( s MyStruct ) void
my_util.rrks
utilize
import my_util
global MyStruct_create() MyStruct {
/* 通常の構造体を基に変数sを宣言してそれを確保 */
varia s MyStruct = {}
return s
}
global MyStruct_print( s MyStruct ) void {
Rrk_print( \=s.i \n ) /* OK */
Rrk_print( \=s.j \n ) /* OK */
Rrk_print( \=s.k \n ) /* OK */
}
不完全構造体MyStructに対応する完全構造体MyStructImplの定義は一つでなければなりません。 例えば今仮にmy_util2.rrksというファイルを別途定義し、 同じMyStructImplという名前で(しかし内容は別の)以下のような完全構造体を定義したとします。
my_util2.rrks
utilize
import my_util
/* もう一つの(MyStructに対する)完全型構造体MyStructImpl */
struct MyStructImpl : MyStruct{
i int /* iが1番目 */
j int /* jが2番目 */
k int /* kが3番目 */
m int
}
このような定義を含むmy_util2.rrksをmy_util.rrksと一緒にコンパイルする場合、 各rrkoファイルの生成までは成功しますが、最後にそれらをリンクするフェーズにおいて 完全構造体が重複定義されているといった旨のエラーが発生します。
実際には完全構造体の名前に対応する特殊な関数エクスポートがrrko内に登録されるため、
そのような(同名の)関数が二重に存在するといったエラーになります。
また不完全構造体MyStructに対応する完全構造体MyStructImplの定義が一つも存在しない場合も 同様にリンクするフェーズにおいて完全構造体が定義されていないといった旨のエラーが発生します。
実際には完全構造体の名前に対応する特殊な関数インポートがrrko内に登録されます。
またこのときそれに対応する(完全構造体の名前に対応する)関数エクスポートがrrko内に一つも登録されていないため、
そのような関数が定義されていないといったエラーになります。
通常、不完全型構造体はrrkhファイル、対応する完全構造体はrrksに記述しますが、
これらを同じファイルに記述することも一応許可されます。
例えば以下の通りです。
ただしこの場合、このファイルに完全構造体も定義されていますので、 このファイル内のどこからでも^impl演算子でそのメンバにアクセスできてしまいます。 また、不完全型構造体がこのファイル以外には公開されていない形になりますので、他のファイルからこの構造体を使用することはできません。 そのため、(最初から通常の構造体で宣言すればよいだけですので)実質的にはあまり意味のない記述です。
不完全型構造体と対応する完全構造体はどちらもグローバルスコープにあるか、あるいは同じブロック内になければなりません。 例えば以下のように、不完全型構造体と対応する完全構造体が異なるブロックにある場合はコンパイルエラーとなります。
/* 不完全型構造体と完全構造体を同じファイルに書く */
struct MyStruct->MyStructImpl{}
struct MyStructImpl : MyStruct{
i int
j int
k int
}
ただしこの場合、このファイルに完全構造体も定義されていますので、 このファイル内のどこからでも^impl演算子でそのメンバにアクセスできてしまいます。 また、不完全型構造体がこのファイル以外には公開されていない形になりますので、他のファイルからこの構造体を使用することはできません。 そのため、(最初から通常の構造体で宣言すればよいだけですので)実質的にはあまり意味のない記述です。
不完全型構造体と対応する完全構造体はどちらもグローバルスコープにあるか、あるいは同じブロック内になければなりません。 例えば以下のように、不完全型構造体と対応する完全構造体が異なるブロックにある場合はコンパイルエラーとなります。
struct MyStruct->MyStructImpl{}
{
struct MyStructImpl : MyStruct{
i int
j int
k int
}
}
関数をメンバにもつ構造体
前項では関数と構造体を別々に扱ってきました。 そのようにしても十分にモジュール化は可能ですが、別の手法としてこれら二つを融合させたアプローチもあります。
これは構造体内に関数型のメンバを持たせることで実現します。 このときデフォルトメンバ初期化子の右辺に無名関数を与えると便利でしょう。 例えば以下のようになります。
my_util.rrkh
struct FStruct->FStructImpl{
/* 関数メンバgetM */
getM = ^fun( self FStruct ) int { ..0 }
/* 関数メンバsetM */
setM = ^fun( varia self FStruct, m int ){}
}
global FStruct_create() FStruct
my_util.rrks
utilize
import my_util
struct FStructImpl : FStruct {
/* 関数メンバgetM */
getM = ^fun( self FStruct ) int {
return self.m
}
/* 関数メンバsetM */
setM = ^fun( varia self FStruct, m int ){
self.m = m
}
/* privateメンバm */
m = 0
}
global FStruct_create() FStruct
{
varia my FStructImpl = {}
return my
}
main.rrks
import my_util
varia fs = FStruct_create()
varia x = fs.~getM() /* シンタックスシュガー: 「fs.getM( fs )」 と書いたのと同じ */
fs.~setM( 6 ) /* シンタックスシュガー: 「fs.setM( fs, 6 )」 と書いたのと同じ */
無名関数で関数型のメンバの実体を定義していますが、ここでこの無名関数の第1番目の引数の型をFStruct型にします。 そしてこの第1番目の引数を介して、例えば構造体のプライベートメンバにアクセスするような記述ができます(上の例では「self.m」)。
ただこのように構造体の内部に関数型のメンバを置くと(C言語などでもそうですが) これを関数として呼び出すときに同じ構造体変数を冗長に書く必要が生じます。 例えば上記の例でgetMを呼び出す場合は「fs.getM( fs )」、setMを呼び出す場合は「fs.setM( fs, 6 )」といった形になります。
このような場合、特殊な演算子「.~」を使うことで構造体変数の登場が1回だけで済む形で記述することができます。 この演算子は関数型メンバの第1引数がそのメンバの親となる構造体変数であるとき、 その第1引数の指定を省略できるというものです。 書式は、「構造体変数.~関数型メンバ( 引数の羅列 )」のようになり、「引数の羅列」では第1引数をスキップして第2引数から指定します (第2引数以降がない場合、これは空になります)。 例えば上記の例でgetMを呼び出す場合は「fs.~getM()」、setMを呼び出す場合は「fs.~setM( 6 )」のように記述できます。
少し抽象化した公式のように書くとするならば「X.~Y(...)」という記述は、「X.Y(X, ...)」という記述とほぼ同じになります。
ただし厳密に言えば「X.~Y(...)」と「X.Y(X, ...)」は全く同じではありません。
例えば以下のコードをご覧ください。
上記でgetStruct()はFStruct型を返します。
ここで /* A */ とある「.~」を用いた呼び出しの場合、getStruct()が実行されるのは1回になります。 それに対して /* B */ とある方では、コード上からも明らかな通りgetStruct()が実行されるのは2回になります。
「.~」を用いた呼び出しは、より正確には以下のように一旦一時変数tmpにgetStructの結果を格納し、 そのtmpに対して「tmp.getM( tmp )」のように記述したのと同じ効果になります。
struct FStruct {
getM = ^fun( self FStruct ) int {
return self.m
}
setM = ^fun( varia self FStruct, m int ){
self.m = m
}
m = 0
}
function getStruct()->{
static varia st_myc FStruct = {}
Rrk_print( "getStruct\n" )
return st_myc
}
getStruct().~getM() /* A */
getStruct().getM( getStruct() ) /* B */
上記でgetStruct()はFStruct型を返します。
ここで /* A */ とある「.~」を用いた呼び出しの場合、getStruct()が実行されるのは1回になります。 それに対して /* B */ とある方では、コード上からも明らかな通りgetStruct()が実行されるのは2回になります。
「.~」を用いた呼び出しは、より正確には以下のように一旦一時変数tmpにgetStructの結果を格納し、 そのtmpに対して「tmp.getM( tmp )」のように記述したのと同じ効果になります。
varia tmp = getStruct()
tmp.getM( tmp )
「.~」演算子では「X.~Y」といったように「( )」を省略して書くことができます。
このとき、Y が関数型のメンバどうかによってこの演算子のふるまいが変わります。
もしも Y が関数型ならば「X.~Y( )」と引数なしで指定したのと同じ意味になり、つまり関数呼び出しになります。
一方、Y が関数型でないならば、「X.Y」と指定したのと同じ意味になり、単なる構造体のメンバへのアクセスとなります。
ちなみに「.」演算子で「X.Y」と指定した場合は、Yが関数型であるかどうかに関わらず、それは常に構造体のメンバYへの直接のアクセスとなります。
また「X.Y」は左辺値となることができますが、「X.~Y」は左辺値となることができません。 つまり「X.Y=10」といった代入文は記述可能ですが、「X.~Y=10」といった代入文を書いた場合はコンパイルエラーとなります。
この性質により、例えばメンバ Y が最初はアクセサではなく、途中でY()といった形のアクセサに変更したとしても、
これを使う側のコードは常に「X.~Y」と記述できることになります。
また「X.Y」は左辺値となることができますが、「X.~Y」は左辺値となることができません。 つまり「X.Y=10」といった代入文は記述可能ですが、「X.~Y=10」といった代入文を書いた場合はコンパイルエラーとなります。
以下のように関数型メンバを別の関数型変数に代入し、それを介して呼び出すことも可能です(ただしその場合は通常の関数呼び出しになりますから、「.~」演算子は使用できません)。
struct FStruct {
getM = ^fun( self FStruct ) int {
return self.m
}
setM = ^fun( varia self FStruct, m int ){
self.m = m
}
m = 0
}
varia fs FStruct = {}
const setM = fs.setM /* 構造体のメンバsetMを別の関数型変数へ代入 */
setM( fs, 7 ) /* 通常の関数呼び出し(「fs.~setM( 7 )」と同じ)*/
最後の行をご覧頂くとお分かりかと思いますが、このようにすると結局「.~」演算子を使って「fs.~setM( 7 )」と記述する替わりに「setM( fs, 7 )」と記述できます。 つまりこのようにすると「.~」演算子を使わずともfsを1回書くだけで済むようにすることはできます。
ブロック単位のモジュール化
不完全型構造体を用いたモジュール化は別のファイルを用意する必要があり、ファイルスコープ単位でしか行うことができません。 これを一つのファイル内においてブロック単位で行うことはできないでしょうか? Rarakuでは通常の構造体のメンバに(無名関数ではなく)lambda式を使うことでこのようなことが実現できます。 例えば以下をご覧ください。
struct Module {
setName = ^lam( name conststr ){}
getName = ^lam()->{ .."" }
print = ^lam(){}
run = ^lam(){}
}
varia my_module = ^fun()->{
varia self Module = {}
/* private member */
varia m_HP = 10
varia m_name = ""
/* private function */
lambda isAlive()->{
.. m_HP > 0
}
/* public member */
self.setName = ^lam( name conststr ) {
m_name = name
}
self.getName = ^lam()->{
..m_name
}
self.print = ^lam(){
if isAlive() {
Rrk_print( "name:" m_name \, "HP:" m_HP \n )
} else {
Rrk_print( "name:" m_name \, "down." \n )
}
}
self.run = ^lam(){
m_HP -= 4
}
return self
}()
my_module.setName( "Raraku" )
my_module.print()
my_module.run()
my_module.run()
my_module.run()
my_module.print()
//my_module.m_name /* アクセス不能: */
上の例では、my_moduleの右辺値となる無名関数(の即時呼び出し)のブロック内で情報隠蔽を実現しています。 最初にModule型のselfを生成しています(このような生成を特にインスタンシエーション(instantiation)と呼びます)。 selfは文字通りこのモジュール自身の実体(これをインスタンス(instance)と呼びます)であり、 この無名関数(の即時呼び出し)の戻り値となります。
次に、無名関数内でローカル変数 m_HP や m_name をインスタンシエーションしますが、これらがmy_moduleのプライベートメンバとなります。 その下にある isAlive はこのモジュール内でのみ使うプライベートな関数です。 ここではこれをlambda関数としていますが、これはプライベートメンバm_HPにアクセスしているためで、 このようなアクセスが必要ない場合はローカル関数としても構いません。
一方、パブリックなメンバについては、selfのメンバに代入することによって それを外部から使えるようにします。 上の例では setName、getName、print、runにそれぞれlambda式を代入しています。 ここでlambda式を使っているのはこれらすべての内部でプライベートメンバにアクセスしているためで、 このようなアクセスが必要ない場合は無名関数としても構いません。
my_moduleの右辺値となる無名関数は即時呼び出しされていますから、my_moduleにはselfの値が格納されることになります。
最後の6行ではこのmy_moduleを実際に使用しています。 ここはもはやモジュールの外部であるため、my_module内のパブリックな関数だけが使用できます。 勿論ここからmy_module内のプライベートメンバにアクセスするような記述はコンパイルエラーとなります。
この例の実行結果は以下のようになります。
name:Raraku, HP:10
name:Raraku, down.
上の例では一旦my_moduleという構造体変数を作ってそれを介してアクセスしましたが、 このような変数を作る必要がない場合であれば、無名ブロックの中にスタティック関数やスタティック変数を宣言して同様のことを実現することもできます。 例えば以下のようになります。
{ /* 無名ブロック */
/* private static variable */
static varia st_HP = 10
static varia st_name = ""
/* private function */
function isAlive()->{
.. st_HP > 0
}
/* public static function */
static setName( name conststr ) {
st_name = name
}
static getName()->{
..st_name
}
static print(){
if isAlive() {
Rrk_print( "name:" st_name \, "HP:" st_HP \n )
} else {
Rrk_print( "name:" st_name \, "down." \n )
}
}
static run(){
st_HP -= 4
}
}
setName( "Raraku" )
print()
run()
run()
run()
print()
//st_name = "" /* アクセス不能: */
今回は構造体を使う必要がなくなった分、前回の例より少し簡潔になります。 ここで st_HP、st_name、isAlive がプライベートとなり、この無名ブロックの外側からこれらにアクセスすることはできません。 一方、setName、getName、print、runがパブリックとなり、この無名ブロックの外側からアクセスすることができます。
ただしこの例の場合、setName、getName、print、runの内部でst_HP、st_name、isAliveにアクセスしているため、
この無名ブロックより前の位置からこれらを呼び出すことはできません。
上記の実行結果は一つ前の例と全く同じになるので省略します。
ポリモルフィズム
「functype文とコールバック関数」のセクションで述べたように、 関数型変数を使うと関数の具体的な実装を行う前の段階で、 処理を抽象化して記述することができます。 またこれをコールバック関数と呼びました。
これをさらに押し進め、構造体内部にある関数型メンバをすべてコールバック関数にすることで、 モジュール全体を抽象化することができます。 そのような抽象化されたモジュールに対しては、最終的にそれを具体的に実装して使うわけですが、 その実装の際に多様なコールバック関数を与えることで、モジュール自体の性質を多様に定義できます。
抽象化されたモジュールはインターフェースの一種とみなせますが、 このように固定化されたインターフェースの基で、その実装が多様に定義できる性質をポリモルフィズムと呼びます。
本来、ポリモルフィズムはオブジェクト指向由来の言葉だと思いますが、
Rarakuはオブジェクト指向のためにデザインされた言語ではないため、
このチュートリアルでもあくまでモジュールとしての文脈でこれを使うことにします。
とはいえ(すぐ後で述べますが)クラスに該当するものをまったく作れないというわけでもありません。 ただしオブジェクト指向として特徴づけられる機能の一つに「継承」がありますが、Rarakuのクラスではこれは提供されないため、 やはりオブジェクト指向の意味でのクラスとは呼べないかもしれません。
一応構造体に親子関係を持たせることができますので、親の方をいわゆる抽象クラスのようなものとして扱うことはできます。 しかしこの場合、親と子で共通に保有するメンバであっても、子ではそれを省略せずすべて記述しなければなりません。 また子の初期化において親の初期化処理に相当するものが自動的に行われるといったこともありません。 つまりRarakuではいわゆる差分プログラミングはできません。
とはいえ(すぐ後で述べますが)クラスに該当するものをまったく作れないというわけでもありません。 ただしオブジェクト指向として特徴づけられる機能の一つに「継承」がありますが、Rarakuのクラスではこれは提供されないため、 やはりオブジェクト指向の意味でのクラスとは呼べないかもしれません。
一応構造体に親子関係を持たせることができますので、親の方をいわゆる抽象クラスのようなものとして扱うことはできます。 しかしこの場合、親と子で共通に保有するメンバであっても、子ではそれを省略せずすべて記述しなければなりません。 また子の初期化において親の初期化処理に相当するものが自動的に行われるといったこともありません。 つまりRarakuではいわゆる差分プログラミングはできません。
前項では構造体の関数型メンバにlambda式を登録することによるモジュール化と情報隠蔽について説明しましたが、 これをさらに押し進めますとこのようなポリモルフィズムを実現できます。 以下の例をご覧ください。
/* interface */
functype FT_bark() void
/* super-struct */
struct Animal {
name conststr
bark FT_bark
}
/* sub-struct */
struct Dog : Animal {
name = "Dog"
bark = ^lam(){}
run = ^lam(){}
}
struct Cat : Animal {
/* common */
name = "Cat"
bark = ^lam(){}
}
struct Mouse : Animal {
/* common */
name = "Mouse"
bark = ^lam(){}
setFavorite = ^lam( favorite conststr ){}
}
function Animal_actionAll( anim_ary Animal[] ) void
{
foreach anim : anim_ary {
anim.bark() /* method-call */
}
}
/* 実際の使用 */
varia st_anim_ary Animal[]
Rrk_push_bk( st_anim_ary, Dog_create(100) )
Rrk_push_bk( st_anim_ary, Cat_create() )
Rrk_push_bk( st_anim_ary, Mouse_create("Cheese") )
/* actionAllの実行 */
Animal_actionAll( st_anim_ary )
/* 具体的な実装 */
function Dog_create( varia stamina int ) Dog
{
varia self Dog = {}
/* public method */
self.bark = ^lam(){
Rrk_print( self.name " says " "Bow wow!\n" )
self.run() /* method-call */
}
/* public method */
self.run = ^lam(){
Rrk_print( self.name " is running! stamina : " stamina \n )
if stamina > 0 {
--stamina;
}
}
return self
}
function Cat_create() Cat
{
varia self Cat = {}
/* private */
const sleep = ^lam(){
Rrk_print( " " self.name " is sleeping...\n" )
}
/* public method */
self.bark = ^lam(){
Rrk_print( self.name " says " "Meow meow!\n" )
sleep()
}
return self
}
function Mouse_create( varia m_favorite conststr ) Mouse
{
varia self Mouse = {}
/* public method */
self.bark = ^lam(){
Rrk_print( self.name " says " "Squeak squeak!\n" )
Rrk_print( " I like " m_favorite \n )
}
/* public method */
self.setFavorite = ^lam( favorite conststr ){
m_favorite = favorite
}
return self
}
上の例ではまず構造体Animalを定義していますが、これが抽象化されたモジュールとなります。 続けてこれを具体的に実装した構造体Dog、Cat、Mouseを定義しています。 このとき、これらの構造体のことを特にクラスと呼び、クラスの関数型メンバを特にメソッドと呼ぶことがあります。
厳密にはオブジェクト指向におけるクラスとは異なるものですが、
これとほぼ同じとみなせる文脈においてこの言葉を使うことにします。
各クラスDog、Cat、Mouseの実装とその生成は、それぞれDog_create、Cat_create、Mouse_create関数で行います。 このように各クラスの実装について、その実装と生成を行う関数を一つ用意する必要がありますが、 この関数のことを(そのクラスの)コンストラクタと呼びます。 コンストラクタ内では、そのクラスのメソッドへlambda式を代入しています (例えばコンストラクタDog_create内では、メソッドbarkやrunへそれぞれlambda式を代入しています)。
この例の実行結果は以下のようになります。
Dog says Bow wow!
Dog is running! stamina : 100
Cat says Meow meow!
Cat is sleeping...
Mouse says Squeak squeak!
I like Cheese
構造体のdeleter
Rarakuの構造体はGC(Garbage Collection)によってその寿命が管理されますが、 その寿命が終わる直前に呼び出す関数を構造体の中に記述することができます。 Rarakuではこれを構造体のdeleterと呼びます。
これはC++のデストラクタやSwiftのdeinitにコンセプトとしては近いものです。
ただしすぐ後で述べますがRarakuのdeleterは使用にあたってかなり強い制限があります。
RarakuのGCはリファレンスカウンタ方式であるため、その参照が消滅した時点で構造体は解体されますが、 deleterはそのタイミングで呼びされます。 ほとんどの場合それはコード上で把握可能で、通常これは関数がreturnされる、ないしはプログラムが終了するタイミングとなります。 ただし循環参照が発生している場合は参照が自然消滅しないため、明示的にRrkGC_cleanを実行しなければ deleterが呼び出されない可能性があります。
RarakuのGCはリファレンスカウンタ方式であるため、その参照が消滅した時点で構造体は解体されますが、 deleterはそのタイミングで呼びされます。 ほとんどの場合それはコード上で把握可能で、通常これは関数がreturnされる、ないしはプログラムが終了するタイミングとなります。 ただし循環参照が発生している場合は参照が自然消滅しないため、明示的にRrkGC_cleanを実行しなければ deleterが呼び出されない可能性があります。
構造体のdeleterの書式は「~deleter( 引数名 ) ブロック」という形になり、また必ずstruct文の中に記述しなければなりません。 「~deleter」の部分は必ずこの名前にします。 括弧で囲まれた部分がdeleterの引数ですが、これはdeleterが所属する構造体自身を示す引数名です。
この引数名は(識別子であれば)何でも構いませんが、慣用的にはselfやthisなどがよく用いられます。
このチュートリアルではselfとしておきます。
括弧の中にはこの引数名をただ一つだけ指定し、しかも引数名の後ろには型名は指定しません。 ブロックの部分は通常の関数の定義の中身と同様に記述します。 一つのstruct文の中にdeleterを二つ以上記述することはできません。
deleterの挙動を確認するための例を以下に示します。
struct MyStruct {
i = 10
~deleter( self ){
Rrk_print( "deleter called. self.i=" self.i \n )
}
}
varia my MyStruct = {}
Rrk_print( "test1\n" )
/* my のdeleterはここで呼び出される */
この例の実行結果は以下のようになります。
test1
deleter called. self.i=10
構造体のdeleterはどのような場合に使用するのでしょうか? 例えば以下のようにRrkFileハンドラを内部で持つような構造体があり、 構造体が解体されるタイミングで確実にファイルをクローズしたいような場合です。
import std/file
struct MyStruct {
fp RrkFile^?
~deleter( self ){
RrkFile_close( self.fp )
}
}
function func()
{
varia my MyStruct = {}
my.fp = RrkFile_open( "test.txt", "rb" )
Rrk_print( "test1\n" )
/* my のdeleterはここで呼び出される */
}
func()
ただし構造体のdeleterの呼び出されるタイミングは分かりにくいため、 実際はその使用を極力控える方がよいでしょう。 このようなことはdefer文で間に合う場合も多いですので、可能であればそちらを使うべきです。
またやむを得ず構造体のdeleterを使用する場合でも、その中身の記述は必要最小限に留めるべきです。 万一deleterの中にバグを入れてしまうと、その解析は非常に難しくなります。
構造体のdeleterの中身はどのようなものも記述できるわけではなく、シンプルなものに限られます。 もっと正確に言えば、deleterはintrovertな関数でなければなりません。 introvertな関数とは次のすべてを満たすものです。
- 関数の中に、グローバルスコープにある変数への直接アクセスは一切ないこと。 ただし引数による間接的なアクセスであれば構いません。
- 関数の中に、static変数の宣言は存在しないこと。 ただし引数による間接的なアクセスであれば構いません。
- 関数の中に、他のコンパイル単位にあるグローバル関数の呼び出しが一切ないこと。 ただしネイティブ関数の呼び出しであれば構いません。
- 関数の中に、関数型変数を介した呼び出しが一切ないこと。
- 関数の中に、他の非introvertな関数の呼び出しが一切ないこと。
以下にコンパイルエラーとなるようないくつかの例を列挙します。 例えば以下はdeleterが非introvertな関数となってしまい、コンパイルエラーとなります。
varia st_var = 9
struct MyStruct {
~deleter( self ){ /* 非introvertな関数 */
st_var = 10 /* コンパイルエラー: deleterの中からグローバルスコープの変数st_varにアクセスしようとしている */
}
}
varia my MyStruct = {}
また以下はdeleterがglobal関数を呼び出していますが、その場合も非introvertな関数となりコンパイルエラーとなります。
struct MyStruct {
~deleter( self ){ /* 非introvertな関数 */
global_func() /* コンパイルエラー: deleterの中からグローバル関数を呼び出そうとしている */
}
}
varia my MyStruct = {}
また以下はdeleterが他の非introvertな関数を呼び出しており、コンパイルエラーとなります。
varia st_var = 9
function func1() /* st_varへ直接アクセスしているのでこれは非introvertな関数 */
{
st_var = 10
}
function func2() /* 非introvertな関数を呼び出しているのでこれは非introvertな関数 */
{
func1()
}
struct MyStruct {
~deleter( self ){
func2() /* コンパイルエラー: deleterの中から非introvertな関数を呼び出そうとしている */
}
}
varia my MyStruct = {}
また以下はdeleterが「関数型変数を介した呼び出しが内部に存在する」関数を呼び出しており、 これも非introvertな関数となりますのでコンパイルエラーです。
functype FT() void
varia st_var = 9
function func0()
{
st_var = 10
}
function func1( f FT ) /* 関数型変数を介した呼び出しを行っているのでこれは非introvertな関数 */
{
f()
}
function func2( f FT ) /* 非introvertな関数を呼び出しているのでこれは非introvertな関数 */
{
func1( f )
}
struct MyStruct {
f FT = ^fun(){}
~deleter( self ){
func2( self.f ) /* コンパイルエラー: deleterの中から非introvertな関数を呼び出そうとしている */
}
}
varia my MyStruct = {}
my.f = func0
また以下では関数内でstatic変数を宣言しているような関数を呼び出していますが、 これも非introvertな関数になるためコンパイルエラーです。
functype FT() void
function func1()
{
static varia st_var = 10 /* static 変数の宣言が内部にあるのでこれは非introvertな関数 */
}
struct MyStruct {
~deleter( self ){
func1() /* コンパイルエラー: deleterの中から非introvertな関数を呼び出そうとしている */
}
}
varia my MyStruct = {}
トレイト
Rarakuではトレイト(trait)という仕組みを使うことで、通常の関数をメソッド呼び出しような記述で呼び出すことが可能になります。 トレイトを宣言するには以下のようにtrait文を使用します。
trait MyTrait {
leng( self ) uint
string leng->RrkStr_leng
}
trait MyTrait {
leng( self ) uint
string leng->RrkStr_leng
at( self ) int
string at->RrkStr_at
}
traitという単語自体は「特性」といったような意味しかありませんが、
これが提供する機能は言語によって様々です。
目次に戻る
大域脱出とtry文
大域脱出とは?
Raraku言語にはいわゆる他の言語で言う「例外」に近い機構として、文字列を伴った大域脱出のためのtry文が用意されています。 しかしtry文の説明の前に、まず大域脱出とは何かについて、そしてそのためのsetjmp文、longjmp文というものについて説明しましょう。
ただし、この setjmp文とlongjmp 文は(あまりに単純かつ強力過ぎるため)実際のプログラミングでは不用意に使うべきではありません。
ほとんどの場合、この二つの替わりに(この二つをもっと安全で使いやすくした)try文とRrk_raise関数を使うべきです。
それにも関わらず最初にこの二つの文を紹介するのは、そもそも大域脱出とは何かを初めて学ぶ場合、
極力単純な機能から段階を踏んで見ていく方がわかりやすいと思われるからです。
大域脱出やC++/Javaのtry-catch文、あるいはそれに該当する制御構造について既にある程度知っている方は、 「大域脱出とは?」「関数呼び出しとコールスタック」「大域脱出を行う意味」「longjmp文で文字列を送信しsetjmp文で受信する」 の見出しの説明は読み飛ばし、いきなり「try文の基本」の見出しの説明から読み始めてもよいでしょう。
大域脱出やC++/Javaのtry-catch文、あるいはそれに該当する制御構造について既にある程度知っている方は、 「大域脱出とは?」「関数呼び出しとコールスタック」「大域脱出を行う意味」「longjmp文で文字列を送信しsetjmp文で受信する」 の見出しの説明は読み飛ばし、いきなり「try文の基本」の見出しの説明から読み始めてもよいでしょう。
setjmpはsetとjumpを、longjmpはlongとjump合体させたようなキーワードになっています。
それぞれ「セットジャンプ」「ロングジャンプ」と発音されることが多いと思います。
C言語をご存知の方は、これはC言語におけるsetjmp関数、longjmp関数とよく似た機構を提供するものです。
ただしRarakuではこれらは関数ではなく(値を返さない)文です。
まずsetjmp文を実行し、次にlongjmp文を実行すると、既に実行したsetjmp文の位置に戻るというのが基本的な挙動です。 最も簡単な例は以下のようになります(尚、この例は無限ループしますから実際に試したい方はCtrl+Cのご用意を)。
function func()
{
setjmp; /* ゴール位置のセット */
Rrk_print( "hello.\n" )
/* 無限ループする */
longjmp /* ゴール位置に戻る */
}
func()
上記の例では、同じfunc関数内でsetjmp文とlongjmp文が使用されており、まず最初のsetjmp文が実行されてゴール位置(setjmp文の直後かつRrk_print関数の直前の位置)が RarakuVMに一時的に記憶されます。 次にlongjmp文の実行時にそのゴール位置へとジャンプします。 ゴール位置はsetjmp文の直後ですから、再びRrk_printとlongjmpが実行され、またゴール位置へジャンプします。 これが繰り返され、結果的に上の例は無限ループとなります。
setjmp/longjmp文が引数無しで使われ、しかもその直後が(キーワードではない)識別子で始まる文が来る場合、文末の「;」は必須です。
Rarakuでは殆どの文で「;」を省略できる中、これは数少ない例外となります。
例えば上の例では(引数の無い)setjmp文の直後にRrk_print関数の呼び出しという識別子で始まる文が来ますから、文末の「;」が必要になります。 longjmp文の直後はほとんど問題になりませんが、setjmpの直後が時々問題になります。 面倒であればsetjmp文では必ず「;」を付けるものと考えても構いません。
例えば上の例では(引数の無い)setjmp文の直後にRrk_print関数の呼び出しという識別子で始まる文が来ますから、文末の「;」が必要になります。 longjmp文の直後はほとんど問題になりませんが、setjmpの直後が時々問題になります。 面倒であればsetjmp文では必ず「;」を付けるものと考えても構いません。
ただ、上記の例のように同じ関数内のジャンプであれば、通常はgoto文とラベル文を使うべきです。 一方、setjmpとlongjmpを使うと、異なる関数を飛び越すようなジャンプが可能になります。 ただしそれは、最もおおざっぱに言えば「呼び出し元の関数の向きへ戻るような」ジャンプです。 「呼び出し元の関数へ戻る」と言えばreturn文ですが、これと何が違うのでしょう? まずは以下の例をご覧下さい。
function func2()
{
return /* func2の呼び出し位置に戻る */
}
function func1()
{
Rrk_print( "hello.\n" )
Rrk_print( "world.\n" )
func2() /* func2の呼び出し位置 */
}
func1()
上記はreturn文を使ったシンプルな例です。 当たり前ですが、func2のreturn文を実行した時点で「func2の呼び出し位置」(func2()の直後の位置)に戻ります。 では次に上を少し修正した以下のような例を考えます。
function func2()
{
longjmp /* ゴール位置に戻る */
}
function func1()
{
Rrk_print( "hello.\n" )
setjmp; /* ゴール位置のセット */
Rrk_print( "world.\n" )
/* 無限ループする */
func2()
}
func1()
今度はfunc2の部分がreturn文ではなくlongjmp文となっています。 この場合、func2のlongjmp文を実行した瞬間に(「func2の呼び出し位置」ではなく)「setjmpがセットしたゴール位置」(setjmp文の直後の位置)に戻ります。 つまりlongjmp文ではreturn文とは異なり、関数呼び出しから戻る位置をsetjmp文で自由に変更できるということです。 上記の実行結果は次のようになるでしょう。
hello.
world.
world.
world.
world.
…無限に続く(Ctrl+Cで終了できます)
しかしreturn文との違いはこれだけではありません。 return文では、いかなる場合でも必ず直前の呼び出し元の関数に戻りますが、 longjmp文では、間に複数の関数呼び出しがあった場合はそれをすっとばし、setjmp文で指定した位置に一気に戻ることができます。 まずは以下の例をご覧下さい。
function func3()
{
Rrk_print( "func3\n" )
return /* func3の呼び出し位置に戻る */
}
function func2()
{
Rrk_print( "func2:begin\n" )
func3() /* func3の呼び出し位置 */
Rrk_print( "func2:end\n" )
}
function func1()
{
Rrk_print( "hello.\n" )
Rrk_print( "world.\n" )
func2()
}
func1()
上の例ではfunc1、func2、func3と順番に関数が呼び出され、 そしてfunc3関数においてreturn文が実行されます。 当たり前ですが、これは直前の呼び出し元の関数(func2関数)内の「func3の呼び出し位置」(func3()の直後)に戻ります。 その実行結果は次のようになります。
hello.
world.
func2:begin
func3
func2:end
では次に上を少し修正した以下のような例を考えます。
varia st_end_flag = false
function func3()
{
Rrk_print( "func3\n" )
st_end_flag = true
longjmp /* ゴール位置(func1内)に(途中のfunc2をすっとばして)一気に戻る */
}
function func2()
{
Rrk_print( "func2:begin\n" )
func3() /* func3の呼び出し位置 */
Rrk_print( "func2:end\n" )
}
function func1()
{
Rrk_print( "hello.\n" )
setjmp; /* ゴール位置のセット */
Rrk_print( "world.\n" )
if st_end_flag {
return /* 無限ループを防ぐため、st_end_flagがtrueならここで終了 */
}
func2()
}
func1()
今度はfunc3の部分がreturn文ではなくlongjmp文となっています。 またsetjmpをfunc1内であらかじめ実行しています。 上の例ではfunc1、func2、func3と順番に関数が呼び出され、 そしてfunc3関数においてlongjmp文が実行されます。 この場合、(return文とは異なり)func3のlongjmp文は(途中のfunc2をすっとばして)一気にfunc1内のゴール位置へ戻ります。 その実行結果は次のようになります。
hello.
world.
func2:begin
func3
world.
この実行結果において、「func3」の直後に「func2:end」の表示がなく、いきなり「world.」が表示されています。 つまりlongjmp実行後において、func2関数内の残りである「Rrk_print( "func2:end\n" )」の部分が実行されず、 一気にfunc1関数内の「Rrk_print( "world.\n" )」の直前の位置までジャンプしたということになります。
このような関数の呼び出し階層(コールスタック(call stack)と呼ばれます)の深い部分からの脱出を大域脱出と呼びます。
setjmp文はlongjmp文より先に実行されていなければなりません。
仮にsetjmp文を実行せずにlongjmp文を実行した場合はランタイムエラーとなります。
制御の流れとしては、longjmpが入り口、setjmp(の直後)が出口に相当するわけですが、
setjmpを先に実行することでその出口の位置を予約しておくといったイメージです。
一つのsetjmp文に呼応する複数のlongjmp文が存在するのは問題ありません。 この場合、それぞれのlongjmp文が一つのsetjmp文の位置へ戻る形になります。 これは一つのラベル文に対して複数のgoto文が呼応する関係と似ています。
これとは逆にsetjmp文が複数存在する状況も考えられますが、これについては後述します。
一つのsetjmp文に呼応する複数のlongjmp文が存在するのは問題ありません。 この場合、それぞれのlongjmp文が一つのsetjmp文の位置へ戻る形になります。 これは一つのラベル文に対して複数のgoto文が呼応する関係と似ています。
これとは逆にsetjmp文が複数存在する状況も考えられますが、これについては後述します。
setjmp文、longjmp文はグローバルスコープに書くこともできます。 例えば以下の通りです。
varia try_count = 3
function func()
{
Rrk_print( "func try_count=" try_count "\n" )
longjmp
}
setjmp if try_count > 0 {
--try_count;
func()
}
上の例では、まず下部のsetjmpが実行され、if文のブロックに入ってfunc()が呼び出されます。 func内のlongjmp文から先ほどの(グローバルスコープにおける)setjmp文へとジャンプします。 try_countの値はデクリメントされ続け、0 になった時点でif文の条件がfalseになりますから実行を終了します。
この例の実行結果は以下のようになります。
func try_count=2
func try_count=1
func try_count=0
関数呼び出しとコールスタック
大域脱出についてのおおよそのイメージがつかめたと思います。 せっかくここまで来ましたので、ここは専門用語の使用を恐れずもっと正確にいきましょう。 やや難解ですがここまで読み進められた読者の方であれば大丈夫かと思います。
たとえよく分からなくても、ここは一読してなんとなくイメージが掴めればそれで十分です。
次へ進みましょう。
そもそも実行時(ランタイム時)の関数呼び出しというものは、(RarakuVMに限らず)一般的にコールスタック(call stack)と呼ばれるスタックで管理されます。 このスタックは最初は空ですが、関数が呼び出されるたびに一つずつ新しい要素がプッシュされます。 そしてそのスタックの一番トップの要素に相当する関数が、ちょうど今現在実行している関数に相当します。 一方、今実行中の関数内でreturn文を実行すれば、コールスタックのトップの要素が一つポップ(最後の要素が削除)されます。 これにより先ほどまではトップの一つ下だった要素が新しいトップとなり、現在実行している関数に相当することになります。 このようにどんどんreturnしていけばやがてコールスタックも空に戻ります。
例えば、func1、func2、func3という3つの関数をこの順番で呼び出せば、 そのコールスタックの内容は、模式的には [ func1、func2、func3 ] という3つの要素をこの順番で持つということです。 このとき func3 がコールスタックのトップの要素になり、現在 func3 関数を実行中ということになりますが、 ここでreturn文を実行すると、コールスタックのトップから一つポップ(func3が削除)されます。 コールスタックの内容は [ func1、func2 ] という2つの要素になりますが、 これはfunc2がコールスタックのトップの要素であり、func2が現在実行中であるということを意味します。
一般にコールスタックの内容は、(ソースコードから明らかな場合を除き)実際に実行してみないと予測できないことがあります。 例えばソースコード上で関数が三つしか定義されていないからといって、コールスタックの最大時のサイズが必ず3とは言えません。 例えば以下の例では、func3内に再びfunc2の呼び出しがありますが、この場合コールスタックの最大時の内容は [ func1、func2、func3、func2 ] となります。
function func3()
{
Rrk_print( "func3\n" )
func2( false )
}
function func2( b bool )
{
Rrk_print( "func2\n" )
if b {
func3()
} else {
/* none */
}
}
function func1()
{
Rrk_print( "func1\n" )
func2( true )
}
func1()
あるいは、以下の例ではfunc1一つしかありませんが、その内部にリカーシブコールがあり、コールスタックの最大時の内容は [ func1、func1、func1、func1、func1 ] となります。
function func1( refer n int )
{
if n == 0 {
return
}
Rrk_print( "func1\n" )
--n;
func1( n ) /* recursive call */
}
varia st_n = 5
func1( st_n )
さらに念のための説明をしておきましょう。 ソースコードの字面上のスコープの階層構造がコールスタックの構造と一致するとも言えません。 例えば以下をご覧下さい。
function func0()
{
/* last: none */
}
{
{
{
static func3()
{
func1()
}
}
}
static func1()
{
function func2() /* local function */
{
func0()
}
func2()
}
}
func3()
上の例ではグローバルブロックの中にstatic関数があったり、関数の中にローカル関数なども定義されています。 そしてソースコード上でのブロックの階層の深さは、func0が一番外、func3が一番奥のようになんとなく見えますが、 だからと言ってコールスタックの順番がこれと一致するとは限りません。 実際、上の例で関数の呼び出し順を注意して追って見ますと、func3、func1、func2、func0 ですので、 この例ではむしろfunc0が呼び出し階層的に一番奥になり、 func0が呼び出された時点でのコールスタックは [ func3、func1、func2、func0 ] となります。
ではこのコールスタックの観点から改めて大域脱出とsetjmp文、longjmp文の動きを見てみましょう。 以下の例をご覧下さい(無限ループします)。
function func5()
{
longjmp
}
function func4()
{
func5()
}
function func3()
{
func4()
}
function func2()
{
setjmp;
func3()
}
function func1()
{
func2()
}
func1()
上の例では、func1、func2、func3、func4、func5 と呼び出されますので func5に到った時点でのコールスタックは [ func1、func2、func3、func4、func5 ] となります。 ここでfunc5ではlongjmpが実行されますが、このときsetjmpが設定された関数(この例ではfunc2)がトップに出現するまでの区間、 コールスタックは問答無用でポップされ、結果的に [ func1、func2 ] となります。 つまりlongjmpが一回実行されただけで3回ものポップが瞬時に行われることになります(これをコールスタックを巻き戻すなどと呼ぶことがあります)。 既に説明したように、より正確にはfunc2の再開位置はsetjmpの直後になりますが、RarakuVMではそのための若干の調整も行われます。 大域脱出とは、このようなコールスタックの巻き戻しであるとも言えます。
setjmp文が複数存在する場合、どうなるのでしょう?
例えば上記の例でfunc2とfunc3の両方にsetjmpが設定されていた場合、 コールスタックの巻き戻しにおいて最初に出会うsetjmpの設定された関数はfunc3になります。 コールスタックの巻き戻しの結果は [ func1、func2、func3 ] となります。 (func2にもsetjmpは設定されておりますが、そこまでは巻き戻らないということです)。 ただしfunc3内のsetjmpの効果が何かの理由で消滅した場合(例えばfunc3でreturn文を実行したり、後ほど述べるunsetjmp文をfunc3内で実行するとその効果は消えますが)、 次回のlongjmpではfunc2まで巻き戻ることになります。
さらに一つの関数内にsetjmpが二つ以上存在する状況も想定できますが、それついての詳細は基本を超えるため、 このセクションの最後の方で参考程度に述べます。 当面は一つの関数につき、たかだか一つのsetjmpしかない場合のみを考えます。
例えば上記の例でfunc2とfunc3の両方にsetjmpが設定されていた場合、 コールスタックの巻き戻しにおいて最初に出会うsetjmpの設定された関数はfunc3になります。 コールスタックの巻き戻しの結果は [ func1、func2、func3 ] となります。 (func2にもsetjmpは設定されておりますが、そこまでは巻き戻らないということです)。 ただしfunc3内のsetjmpの効果が何かの理由で消滅した場合(例えばfunc3でreturn文を実行したり、後ほど述べるunsetjmp文をfunc3内で実行するとその効果は消えますが)、 次回のlongjmpではfunc2まで巻き戻ることになります。
さらに一つの関数内にsetjmpが二つ以上存在する状況も想定できますが、それついての詳細は基本を超えるため、 このセクションの最後の方で参考程度に述べます。 当面は一つの関数につき、たかだか一つのsetjmpしかない場合のみを考えます。
最後に練習も兼ねて、再帰呼び出し(recursive call)を伴うケースも見てみましょう。
function func( refer i int )
{
++i;
Rrk_print( "func: i=" i "\n" )
if i >= 3 {
longjmp
}
func( i ) /* recursive call */
}
varia i = 0
varia is_try = true
setjmp if is_try {
is_try = false
func( i )
} else {
Rrk_print( "catch in main : i=" i "\n" )
}
上の例ではまずグローバルスコープから始まりますが、このときのコールスタックはまだ空です。 下部にあるsetjmpが実行され、一回目のfunc関数呼び出しが行われます。 このときコールスタックは [ func ] であり、その要素は一つです。
今回は変数iがrefer型(call by reference)として引数に指定されておりますので、関数内での i の更新が関数外へも反映されることに注意しましょう。 func関数内では i はインクリメントされますが、i が 3 未満の間は func が再帰呼び出しされ続けます。 コールスタックも(すべての要素がfuncですが)一つずつ増加していきます。
i が 3 に至ったタイミングで longjmp が実行されます。 このときコールスタックは [ func、func、func ] であり、その要素は三つでこの例では最大サイズを迎えます。 longjmp文の実行によりこのコールスタックがすべて巻き戻され、(setjmpが実行されたのはグローバルスコープですので) コールスタックは一瞬で空になります。 同時に制御もグローバルスコープにおけるsetjmpの位置へ戻ります。
このとき is_try の値は既にfalseですから else のブロックが実行され、最後のメッセージ「catch in main : i=3」を表示して処理を終了します。
この例の実行結果は以下のようになります。
func: i=1
func: i=2
func: i=3
catch in main : i=3
大域脱出を行う意味
ここまでで大域脱出とはどのような挙動であるかはわかりました。 しかしこれが何の役に立つのでしょう?
例えば関数呼び出しの深い部分でエラーが発生した場合、 大域脱出によって、setjmpがセットされた関数までコールスタックを一気に巻き戻り、 戻った関数の途中の位置(setjmp文の直後)から実行を再開して、そこで一括してエラーハンドリングするといったことができます。 この状況をさらに説明用にシンプルにしたコードが以下となります。
function func3()
{
varia result = false
if !result {
/* ここで何らかのエラーが発生したものと仮定する */
Rrk_print( "func3 : error : something.\n" )
longjmp /* func1におけるsetjmpの位置に一気に戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia once = 0 setjmp if once ? 0 : ++once { /* try */
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error.\n" )
}
}
func1() /* entry point */
上記の例では、まずfunc1内に入ります。 このとき最初にやや奇妙な行「varia once = 0 setjmp if once ? 0 : ++once { /* try */」を実行します。 これについては解説が必要でしょう。 少々分かりにくいですが、ここではvaria文、setjmp文、if文の3つを一行でまとめて書いています。
この処理はこれから何回も慣用句的に出現するので、ここでは敢えて一行で書いています。
ただし実際のプログラミングではこのような表記するのではなく、後で述べるtry文を替わりに使うべきです。
ここでは(言葉であいまいに説明されがちな)try文のやや複雑な制御の流れを(より単純で既知の)setjmp文とif文という具体的なコードで明確にするため、
まずはこのような表記で説明しています。
まず「varia once = 0」で整数onceを宣言し、初期値を0とします。 次に「setjmp」を実行します。最後に「if once ? 0 : ++once {」で、if文の条件式を評価するわけですが、 この三項演算子「once ? 0 : ++once」の部分は最初に実行すると 1、二回目以降に実行すると 0 になります。 そのため、このif文ではまずifブロックの方が実行されます。
この三項演算子「once ? 0 : ++once」が最初は 1 になる理由は以下の通りです。
onceは最初は 0 ですから三項演算子「once ? 0 : ++once」では第3オペランドの「++once」が実行され、その値が評価されます。 「++once」全体の値はonceをインクリメントした後の値ですから 1 となります。 従って「once ? 0 : ++once」全体の値としても 1 になります。
onceは最初は 0 ですから三項演算子「once ? 0 : ++once」では第3オペランドの「++once」が実行され、その値が評価されます。 「++once」全体の値はonceをインクリメントした後の値ですから 1 となります。 従って「once ? 0 : ++once」全体の値としても 1 になります。
if文に入るとfunc2、func3と関数が呼び出されます。 今func3内で何らかのエラーが発生し、そこからlongjmp文で一気にfunc1のsetjmp文の直後へ戻るとしましょう。 ここで再度if文の条件式「once ? 0 : ++once」が評価されますが、 これは二回目の実行ですからその値は 0 になります。 そのため、今度はelseブロックの方が実行されます。
この三項演算子「once ? 0 : ++once」が二回目以降 0 になる理由は以下の通りです。
onceの値は既に 1 になっていますから、今度は第2オペランドの「0」が評価されます。 従って「once ? 0 : ++once」全体の値としても 0 になります。
onceの値は既に 1 になっていますから、今度は第2オペランドの「0」が評価されます。 従って「once ? 0 : ++once」全体の値としても 0 になります。
ここまでまとめますと、一番最初はこのifブロックが必ず実行されます。 そのブロック内部でエラーが発生した場合は今度はelseブロックが実行されます。 逆にエラーが発生しなかった場合は、通常通りifブロック内の処理を全部実行した後にそのブロックを抜けるだけです (勿論この場合はelseブロックは実行されません)。 elseブロックが実行されるということは、「ifブロックの内の何かの関数呼び出しの中で、何かはわからないがとにかく何かのエラーが発生した」ということです。 そして上の例では、その旨の表示をさせる意図でRrk_printによりメッセージを表示しています。 この例の実行結果は以下のようになります。
func3 : error : something.
func1 : catch error.
この処理における売りは、func3で何かエラー値を返さなくても(そして途中で呼び出されたfunc2関数でそのエラー値を呼び出し元へたらい回しに伝播しなくても) 最上層のfunc1でfunc3内の何らかのエラーの発生を感知できるということです。 しかしどんなエラーが発生したのかまでは、これだけではわかりません。 その情報までfunc1側から補足するにはさらなる工夫が必要です。
上の例ではfunc3とfunc1内のRrk_print文でエラーメッセージを直接表示していますが、
このようにしてしまうと仮にRrk_print以外の手段(例えばログファイルに記録)に途中で変更したくなった場合、
これらすべてに修正を加えなければなりません。
一方、純粋にエラーメッセージを文字列だけでfunc1側に伝達できれば、Rrk_printを使うのはfunc1におけるelseブロックの中だけで済みます
(あるいはRrk_printの替わりに何らかのラッパー関数を使うようにしてもこの問題に対処できるでしょう)。
エラーメッセージを文字列としてfunc1側に伝達する手段として、例えばグローバルスコープにエラーメッセージ用の変数を用意したり、 func3とfunc2の双方にエラーメッセージを受け取るための引数を設けるなどの方法が考えられますが、 実はすぐ後に述べるlongjmp文とsetjmp文の引数を使うことでもっと簡単にこれを実現できます。
エラーメッセージを文字列としてfunc1側に伝達する手段として、例えばグローバルスコープにエラーメッセージ用の変数を用意したり、 func3とfunc2の双方にエラーメッセージを受け取るための引数を設けるなどの方法が考えられますが、 実はすぐ後に述べるlongjmp文とsetjmp文の引数を使うことでもっと簡単にこれを実現できます。
longjmp文で文字列を送信しsetjmp文で受信する
longjmp文では引数として文字列型(string型かconststr型)の識別子(変数/定数)を指定し、setjmp文へ送信することができます。 その場合、受信する側のsetjmp文でも引数として文字列型(string型)の識別子(変数/定数)を指定する必要があります(指定しなかった場合、 longjmp文で送信した文字列は単に無視されます)。 引数は間にコロン「:」を入れ、その後ろに指定します。 すなわちlongjmp文の方の書式は「longjmp : 識別子(string型かconststr型)」となり、 一方、setjmp文側の方の書式は「setjmp : 識別子(string型のみ)」となります。 例えば以下の通りです。
function func3()
{
varia result = false
if !result {
const src = "error from func3" /* 必ずstring型またはconststr型でなければならない */
longjmp:src /* func1におけるsetjmpの位置に文字列srcを送信しつつ戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia dst string /* 必ずstring型でなければならない */
/* setjmp:dstの部分でlongjmpからの文字列を受け取る */
varia once = 0 setjmp:dst if once ? 0 : ++once { /* try */
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error [" dst "].\n" )
}
}
func1() /* entry point */
上記では「setjmp:dst」により、エラー文字列の受け手となるstring型変数dstをまず指定し、 また「longjmp:src」により、エラー文字列の送り手となるconststr型定数srcを指定しています。
これが実行されるとfunc1内のsetjmpに戻りますが、そのとき(setjmp文で指定した)func1側のdst(string型の変数)に、 longjmp文で指定した文字列srcの値がディープコピーされます(シャローコピーではありません)。
上記の例で、この二つの変数は最初から異なる実体を指しており、longjmpを実行してもその指し示す先は同じ実体のままです。
変更されるのはその実体の持つ値です。
これはちょうど RrkStr_set( dst, src ) が実行されたのと同じ状況です。
グローバル変数や関数の引数を介してsetjmp/longjmpの引数に(実体が)同一の変数/定数を意図的に指定することもできますが、 そもそも双方から同一の変数にアクセスできる状況ならば、単に(普通の方法で)その変数にエラーメッセージを代入すれば済む話ですから、 その場合setjmp/longjmpの引数を利用する必要性もないことになります。
この見出しではデータの流れの向きを強調するために「送信/受信」という若干大げさな言葉を使いましたが、
もちろん内部でネットの通信をしているわけではなく、やっていることはただの文字列のコピーです。
グローバル変数や関数の引数を介してsetjmp/longjmpの引数に(実体が)同一の変数/定数を意図的に指定することもできますが、 そもそも双方から同一の変数にアクセスできる状況ならば、単に(普通の方法で)その変数にエラーメッセージを代入すれば済む話ですから、 その場合setjmp/longjmpの引数を利用する必要性もないことになります。
その実行結果は次のようになります。
func1 : catch error [error from func3].
この実行結果より、確かにfunc3側のsrcからfunc1側のdstへ文字列のコピーが行われているのが確認できます。
setjmp/longjmpのうち、片方にだけ引数が設定されており、もう片方には引数が設定されていない状況も許可されます。
例えばsetjmpの方だけに引数指定されている場合、longjmpが実行されてもその引数の値は何も変化しません。
逆にlongjmpの方だけに引数指定されている場合、longjmpが実行されてもsetjmp側では単にそれを無視します。
try文の基本
今まで毎回「varia once = 0 setjmp:ex if once ? 0 : ++once { /* try */」のように書いてきましたが、 Rarakuではtry文を使うことで、これに相当する処理を簡易に記述できます。
実際、Rarakuコンパイラでもtry文をこれに相当する処理で内部的に展開し、
さらに色々な問題に備えて調整をすることで実装しています。
厳密に言えば「setjmp:ex」ではなく「setjmp*:ex」に相当するものに展開されます。
setjmp文に「*」が付いた場合の意味については基本を超えるため、
このセクションの最後で参考程度に述べます。
これを使うと今までの例は以下のように記述できます。
function func3()
{
varia result = false
if !result {
Rrk_raise( "error from func3" ) /* func1におけるtry文の位置に文字列を送信しつつ戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia ex string /* tryの引数に指定する. 必ずstring型でなければならない */
/* exがRrk_raise関数からの文字列を受け取る */
/* この「try ex」は varia once = 0 setjmp*:ex if once ? 0 : ++once とおおよそ同じ */
try ex {
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error [" ex "].\n" )
}
}
func1() /* entry point */
C++/Javaなどをご存知の方は、Rarakuのtry文(正確にはtry-elif-else文)は、
C++/Javaにおけるtry-catch文に対応するものと考えて差し支えないと思います。
つまりRarakuにおけるtry文がC++/Javaにおけるtry、elif文やelse文がC++/Javaにおけるcatchに相当します。
ただしC++/Javaのtryとは違って、Rarakuのtry文には引数を設定することもあります。 またC++/Javaのcatchでは例外クラス等を引数に指定しますが、Rarakuの場合そもそもクラスというものが存在しません。 替わりにtry文で取得した文字列の内容に応じてelif文で切り分けるといったことで同様のことを行います
またRrk_raise関数は、C++/Javaにおけるthrow文に相当します。 Rarakuでは「投げる」のは例外ではなくただの文字列です。
ただしC++/Javaのtryとは違って、Rarakuのtry文には引数を設定することもあります。 またC++/Javaのcatchでは例外クラス等を引数に指定しますが、Rarakuの場合そもそもクラスというものが存在しません。 替わりにtry文で取得した文字列の内容に応じてelif文で切り分けるといったことで同様のことを行います
またRrk_raise関数は、C++/Javaにおけるthrow文に相当します。 Rarakuでは「投げる」のは例外ではなくただの文字列です。
ここを文字列としたのは、必要ならそれをパースする関数等を用意することで、いくらでも柔軟に切り分けできるだろうと考えたからです。
ライブラリ側で世にあるのすべてのエラー(や例外に相当するもの)のクラスを網羅するのは土台無理な話ですし、 あるいはそれらの範疇を(スーパークラスに相当するもので)上手く分類しようと試みてガチガチに固めたとしても、 後で何らかの歪みや問題が発生しないとも限りません。 さらにはエラークラスの分類が複雑になればなるほど、ユーザはおそらくその全容を把握する努力をするより、 すべてのエラークラスのトップに君臨するスーパークラスを使おうとするでしょう(つまりエラーを揉み消す方向に努力を捧げることになるでしょう)。 結局、このあたりに凝った仕掛けを設けようにもそもそも方向性を決め兼ねる部分が多いため (どうやってもどのみち何らかの問題は発生する気がします)、 Rarakuの実装ではそこに労力を向けることはせず、いっそのこと全部文字列にしてしまう方針にしました。
ライブラリ側で世にあるのすべてのエラー(や例外に相当するもの)のクラスを網羅するのは土台無理な話ですし、 あるいはそれらの範疇を(スーパークラスに相当するもので)上手く分類しようと試みてガチガチに固めたとしても、 後で何らかの歪みや問題が発生しないとも限りません。 さらにはエラークラスの分類が複雑になればなるほど、ユーザはおそらくその全容を把握する努力をするより、 すべてのエラークラスのトップに君臨するスーパークラスを使おうとするでしょう(つまりエラーを揉み消す方向に努力を捧げることになるでしょう)。 結局、このあたりに凝った仕掛けを設けようにもそもそも方向性を決め兼ねる部分が多いため (どうやってもどのみち何らかの問題は発生する気がします)、 Rarakuの実装ではそこに労力を向けることはせず、いっそのこと全部文字列にしてしまう方針にしました。
ここでfunc3内の変化にも注意してください。 今までは「longjmp:ex」を使ってきましたが、上記の例ではこれが「Rrk_raise( "error from func3" )」に変更されています。 つまりtry文を使用する場合はRrk_raise関数を使う必要があります。
実はRrk_raiseはlongjmp文のラッパーで、以下のように定義されています。
よく見ると単なるlongjmpではなくその後ろに「*」がついています。 このようにすると、longjmpを行った直後、func1内におけるtry文のelseブロックに到達した時点で、 try文におけるsetjmpの効果を消滅させることができます。 (通常、elseブロックに到達した後は、try文におけるsetjmpの効果は完全に不要ですから(というより効果が残っていると場合によってはむしろ邪魔になりますから)、この方が望ましいわけです)。
ただし実際のプログラミングでは、「longjmp*」を直接使うのではなくRrk_raiseの方を使うべきです。 まず「longjmp*」では「*」を付け忘れるリスクがあります。 また「longjmp*」では引数を付ける場合は必ず単独の識別子を指定しなければなりませんが、 Rrk_raiseであれば文字列リテラルや文字列連結式を直接指定することも可能です。
ちなみにtryブロックでは、それが最後まで無事終了した時点で(そのtry文における)setjmpの効果が消滅します。 つまりtry-else文が完全に終わった後は(どのブロックを経由したにせよ)setjmpの効果は消滅していることになります。
function Rrk_raise( ex conststr )
{
if ex {
longjmp*:ex
}:{
longjmp*
}
}
よく見ると単なるlongjmpではなくその後ろに「*」がついています。 このようにすると、longjmpを行った直後、func1内におけるtry文のelseブロックに到達した時点で、 try文におけるsetjmpの効果を消滅させることができます。 (通常、elseブロックに到達した後は、try文におけるsetjmpの効果は完全に不要ですから(というより効果が残っていると場合によってはむしろ邪魔になりますから)、この方が望ましいわけです)。
ただし実際のプログラミングでは、「longjmp*」を直接使うのではなくRrk_raiseの方を使うべきです。 まず「longjmp*」では「*」を付け忘れるリスクがあります。 また「longjmp*」では引数を付ける場合は必ず単独の識別子を指定しなければなりませんが、 Rrk_raiseであれば文字列リテラルや文字列連結式を直接指定することも可能です。
ちなみにtryブロックでは、それが最後まで無事終了した時点で(そのtry文における)setjmpの効果が消滅します。 つまりtry-else文が完全に終わった後は(どのブロックを経由したにせよ)setjmpの効果は消滅していることになります。
尚、文字列を送信/受信する必要がない場合、try文とRrk_raise関数を以下のように記述することもできます。
function func3()
{
varia result = false
if !result {
Rrk_raise( null ) /* func1におけるtry文の位置に戻る */
}
}
function func2()
{
func3()
}
function func1()
{
/* この「try」は varia once = 0 setjmp* if once ? 0 : ++once とおおよそ同じ */
try {
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error.\n" )
}
}
func1() /* entry point */
try文におけるdefer文
try文のブロックの中に、手動で解放する必要のある変数が存在する場合を考えます。
例えばtry文のブロックの中に、fp(RrkFile型)をオープンするRrkFile_openとクローズするRrkFile_closeが存在する場合、 これをあまり考えなく使ってしまいますと Rrk_raise関数の実行によってRrkFile_closeの呼び出しがスキップされるかもしれません (つまり、fpがクローズされずに残るかもしれません)。
実際のコードで見てみましょう。 例えば以下は(意図的にこのようにしてないのであれば)問題のあるコードです。
import std/file
function func3()
{
Rrk_raise( null ) /* func1におけるtry文のelseへ戻る */
}
function func2()
{
varia fp = RrkFile_open( "data.txt", "rb" )
if fp == null {
Rrk_print( "func2: Cannot open file.\n" )
Rrk_raise( null ) /* func1におけるtry文のelseへ戻る */
}
func3()
RrkFile_close( fp ) /* 実行されない */
}
function func1()
{
try {
func2()
} else {
Rrk_print( "func1: catch error\n" )
}
}
func1()
上記の例ではfunc3のRrk_raiseにより、func1におけるtry文(のelseブロック)へ戻りますが、 このときfunc2におけるRrkFile_close( fp )は実行されません。
これが実行されなかったとしても、fp はfunc2の最後でどこからも参照されなくなるため、
一応GCによって自動的に解放されます。
しかし一般にコードに明示的に記述された解放用の関数は、プログラマがその実行を意図して記述したものであるはずですから、
それが実行されないという状況は望ましいことではありません。
以下のようにfpを解放する必要がある関数内でさらにtry文を置けば、一応この問題は回避できます。
import std/file
function func3()
{
Rrk_raise( null ) /* func2におけるtry文のelseへ戻る */
}
function func2()
{
varia result = false
varia fp = RrkFile_open( "data.txt", "rb" )
if fp == null {
Rrk_print( "func2: Cannot open file.\n" )
goto FUNC_END
}
try {
func3()
} else {
Rrk_print( "func2: catch error\n" )
goto FUNC_END
}
result = true
FUNC_END: /* finally */
RrkFile_close( fp )
if result == false {
/* re-raise */
Rrk_raise( null ) /* func1におけるtry文のelseへ戻る */
}
}
function func1()
{
try {
func2()
} else {
Rrk_print( "func1: catch error\n" )
}
}
func1()
ただしコードはやや煩雑となってしまいます。 このケースでは呼び出し階層がさほど深くないですし、むしろtry文を使わず以下のように素直に戻り値をチェックする方法にした方が 何が起きているかわかりやすく書けるでしょう。
import std/file
function func3() bool
{
return false
}
function func2() bool
{
varia result = false
varia fp = RrkFile_open( "data.txt", "rb" )
if fp == null {
Rrk_print( "func2: Cannot open file.\n" )
goto FUNC_END
}
if !func3() {
Rrk_print( "func2: catch error\n" )
goto FUNC_END
}
result = true
FUNC_END: /* finally */
RrkFile_close( fp )
return result
}
function func1()
{
if( !func2() ){
Rrk_print( "func1: catch error\n" )
}
}
func1()
しかしそもそもtry文を使いたい状況というのは、呼び出し階層が深く、逐一戻り値チェックを行うのが困難である場合ですから、 その観点から言えばこれは実質的な解決になっていません。
このような場合、最も良いのはdefer文を用いる方法です。 Rrk_raise関数の呼び出しにより、コールスタックは一つずつ巻き戻りながら最終的にtry文が存在する要素に相当する関数に到りますが、 実はこの巻き戻し処理中、途中の関数でdefer文が指定されていた場合は、そのdefer文で指定した関数呼び出しがその場で自動的かつ必ず行われます。
これはtry文とRrk_raise関数の仕様というより、それらを使った時に内部で実行されるsetjmp/longjmpの仕様です。
longjmp文の実行により、コールスタックは一つずつ巻き戻りながら最終的にsetjmp文が存在する要素に相当する関数に到りますが、
実はこの巻き戻し処理中、途中の関数でdefer文が指定されていた場合は、そのdefer文で指定した関数呼び出しがその場で自動的かつ必ず行われます。
そのため、結果的にtry文とRrk_raise関数でも全く同様のことが起きるということです。
これを利用して、一番最初のtryを用いたような状況を以下のように記述することができます (ただし以下では実際にRrkFile_open、RrkFile_closeを呼び出してはいません)。
function Dmy_open( filename conststr, mode conststr ) int /* RrkFile_openのようなものを想定 */
{
const dmy = 1
Rrk_print( "Dmy_open : " \=dmy\n )
return dmy
}
function Dmy_close( dmy int ) /* RrkFile_closeのようなものを想定 */
{
/* none */
Rrk_print( "Dmy_close : " \=dmy\n )
}
function func3()
{
Rrk_print( "func3.\n" )
Rrk_raise( null ) /* func1におけるtry文のelseへ戻る */
}
function func2()
{
if fp := Dmy_open( "data.txt", "rb" ); {
defer Dmy_close( fp ) /* defer文: これはRrk_raise時にも実行される */
func3()
} else {
Rrk_print( "func2: Cannot open file.\n" )
}
Rrk_print( "func2: end.\n" )
}
function func1()
{
try {
func2()
} else {
Rrk_print( "func1: catch error\n" )
}
}
func1()
上記では呼び出された状況をRrk_printで確認表示するため、
RrkFile_open、RrkFile_closeの替わりにDmy_open、Dmy_closeを作ってそれを使っています
(またそれに伴って、これらの関数の戻り値や引数をRrkFile型ではなく、替わりに単なるint型にしています)。
実際には上記の例において Dmy_openをRrkFile_open、 openCloseをRrkFile_closeに置き換えるなどすればよいでしょう。
実際には上記の例において Dmy_openをRrkFile_open、 openCloseをRrkFile_closeに置き換えるなどすればよいでしょう。
上記ではDmy_openの直後にdefer文でDmy_closeの呼び出しを指定していますが、 この場合、func3におけるRrk_raise関数の実行によってまずfunc2へ巻き戻り、このとき同時にdefer文で指定されたDmy_closeが自動的に呼び出されます。 さらにその呼び出しが終るとfunc1へ巻き戻り、try文(のelseブロック)の位置へ至ります。 実行結果は次のようになります。
Dmy_open : dmy=1
func3.
Dmy_close : dmy=1
func1: catch error
「func3」から「func1: catch error」の間に「func2: end.」という表示が見られないため func3からfunc1へジャンプしていることが分かりますが、 一方でfunc2においてdefer文で指定したDmy_closeについては、確かに(そのジャンプの過程で)実行されていることも確認できます。
このようにdefer文を使うと、中間でtry文を入れたりRrk_raiseを再度実行するような処理が不要になります。
try文のブロック内におけるgoto文/break文/continue文/fallthrough文
try文のブロックの配下でgoto文を用いる場合、そのtry文のブロックの外にあるラベルへ向かって(gotoによる)ジャンプをすることはできません。 例えば以下のような状況は許可されません。
try {
{
goto L /* NG: try文のブロックの外にあるラベルLへgotoしようとしている */
}
}
L: /* try文のブロックの外 */
一つのtryブロックが終了した直後、RarakuVMは適切な後処理を内部で行いますが、
gotoで強引にtryブロックの外に出てしまうと、この後処理が実行されなくなってしまいます。
そのため、Rarakuではこのような記述を禁じています。
逆に言えば、そのtryブロックの外へ抜け出るようなgotoでなければ(通常のgotoの規則の範囲内で)許可されます。 例えば以下はOKです。
try {
{
goto L /* OK: try文のブロックの外に抜け出るようなgotoではない */
}
L: /* try文のブロックの中 */
}
あるtry文のブロックの配下でbreak文やcontinue文を用いる場合、必ずその親に該当するwhile/do/for/foreach文も それと同じtry文のブロック内になければなりません。 例えば以下のような状況は許可されません。
while true {
try {
break /* NG: このbreakの親に該当するwhile文(whileキーワードが使われている部分)が、このtryブロックの内部にはない. */
}
}
これが許可されない理由も、goto文の場合と同じです。
逆に言えば、そのtryブロックの配下に、親に該当するwhile/do/for/foreach文もあるなら許可されます。 例えば以下はOKです。
try {
while true {
break /* OK: tryブロックの配下にあるbreakの親に該当するwhile文も、同じtryブロックの配下にある. */
}
}
break文やcontinue文の場合と全く同様に、 あるtry文のブロックの配下でfallthrough文を用いる場合、必ずその親に該当する(switch文における)case/default文も それと同じtry文のブロック内になければなりません。 例えば以下のような状況は許可されません。
const kind = 1
switch kind {
case 1:
try {
fallthrough /* NG: このfallthroughの親に該当するcase文(caseキーワードが使われている部分)が、このtryブロックの内部にはない. */
}
default:
}
逆に言えば、そのtryブロックの配下に、親に該当するswitch文もあるなら許可されます。 例えば以下はOKです。
const kind = 1
try {
switch kind {
case 1:
fallthrough /* OK: tryブロックの配下にあるfallthroughの親に該当するswitch文も、同じtryブロックの配下にある. */
default:
}
}
ちなみに「switch kind {」と「case 1:」の間に「try {」が来るような状況は
今度はswitch文の文法に違反するため、許可されません。
try文のブロックをまるであたかも一つの無名関数であるかのようにみなし、それとのgoto/break/continue/fallthroughの関係を考えれば、
この一連の規則は覚えやすいでしょう。
実際、無名関数にも全く同様の規則があります。
ただし念のために補足しますが、これは「そう考えると覚えやすい」というだけです。 try文のブロックの実体はあくまでブロック(内部的にはifブロックとよく似た性質を持つ特殊なブロック)であって、 無名関数や通常の関数定義とは全く異なるものです。 例えばtry文のブロックの中でreturnするのと無名関数の中でreturnするのとでは、挙動は全く異なります。
ただし念のために補足しますが、これは「そう考えると覚えやすい」というだけです。 try文のブロックの実体はあくまでブロック(内部的にはifブロックとよく似た性質を持つ特殊なブロック)であって、 無名関数や通常の関数定義とは全く異なるものです。 例えばtry文のブロックの中でreturnするのと無名関数の中でreturnするのとでは、挙動は全く異なります。
ちなみにtry文のブロックの中でreturnするのは許可されます。
一つのtryブロックが終了した直後、RarakuVMは適切な後処理を内部で行うと説明しましたが、
この後処理とは、try文の内部で実行されたsetjmp文の効果を打ち消すというものになります。
そしてこれと同じことはreturn文の実行においても結局行われるためです。
try文を用いる場合のその他の注意
グローバル関数の配下の階層の関数内でRrk_raise関数を使い、かつtry文でそれを捕捉しきれていない形になっている場合、 そのままでは他のrrksへまで大域脱出の効果が及びます。 つまり結果的にグローバル関数のユーザにtry文でそれを捕捉することを強要させてしまいますが、通常これは望ましい設計ではありません。 このような状況を防ぐため(Rrk_raiseを使っている場合は)階層の出口で確実にtry文で捕捉し、グローバル関数自体の外部へのエラー伝播は戻り値ベースとすべきです。
function static_func2()
{
Rrk_raise( null ) /* global_funcにおけるsetjmpへ戻る */
}
function static_func1()
{
static_func2()
}
/* 他のユーザへ公開するための関数 */
global global_func() bool
{
/* グローバル関数の外にまで大域脱出の効果が及ばないようにここで確実に捕捉 */
try {
static_func1()
} else {
Rrk_print( "global_func: catch error\n" )
return false
}
return true
}
try文で行われるような処理は関数の戻り値をチェックしつつ、一つずつ上の階層へ戻るという手法でも可能です。
特に汎用で利用するためのglobalなライブラリ関数などを作成してユーザに提供する場合、 その関数のユーザにこのような大域脱出やtry文を使うことを強いる仕様にすべきではありません。 少なくともその窓口となる戻り値と引数ベースで、成功/失敗、エラーの種類、エラーメッセージを取得できるようにしておくのがおそらく一番健全で、 実際のライブラリでもそうなっている場合も多いと思います (数学用ライブラリのようなやむ得ない場合もありましょうが)。
この場合、戻り値は成功/失敗を表すbool値とするのがおそらく一番よいでしょう。 逆に言えば、戻り値の型はやむを得ない場合を除きあまり凝ったものにしない方がよいと思います。 例えば戻り値の型をenum型などにすることもできますが、その場合呼び出し元はswitch文(あるいはif-elifの連発)等で切り分けることになります。 これは単純なif文で判定するよりもほとんどの場合かなり手間になります。 関数のユーザはおそらくその成功/失敗程度であればif文で判定してくれるでしょうが(それすら面倒でしない場合もありましょうが)、 絶対に必要な状況を除いておそらく以下のようにわざわざswitch文(やそれに該当する記述)まで持ち出して判定してはくれないでしょう (何よりこれを漏れなくswitch文で網羅するには、ユーザはそのenum型の意味をすべて把握する必要があります)。
このような場合(つまりエラーの種類に応じて何か特別な処置をするのではなく単にエラーの種類を知りたい場合ですが)、 エラーメッセージを追加するための文字列型の引数を用意するのが、おそらく多くの場合でバランスがとれた方法のように思います。 引数で指定するため必ず強制されます(従って受け取り漏れの心配は通常ありません)し、たかがstringの変数を一つ用意する程度あればたいした手間でもないからです。
一方、汎用でないプライベートな関数(global指定されていない関数)などを作成する場合であれば、(それらの関数の)インターフェース自体はそこまで堅牢にする必要はないかもしれません。 そのソースコードを見るのはその実装者に限定され、プライベートな関数は外のソースコードからもアクセスもできませんから、 インターフェースも好きにすればよいわけです。
今このプライベートな実装において、関数呼び出し階層が非常に深く、かつ複雑な再帰呼び出しを伴うような(たとえばパーザなどの実装では起こりがちですが)状況である場合を考えましょう。 関数呼び出し階層の深い部分でエラーが発生したとき、その時点で強制終了できる状況であれば簡単なのですが、 それはできず関数呼び出し階層の一番最上層までとりあえずエラーを伝播し、そこで適切な処置をしなければならない状況とします。
このときその階層に介在するほとんどの関数で戻り値チェックを行い、さらにそれに従ってエラー情報を呼び出し元の上層へたらい回し的に伝播させねばなりません。 これを漏れなく行うのは大変な手間になりますし、そもそも漏れがあった時点で最上層へのエラーの伝播が失敗します(つまりバグです)。 またコードも煩雑になりますので、このことで副次的なバグを生むかもしれません。 このような場合、大域脱出を使った方がむしろ確実です。
逆に言えば、そのような猛烈に複雑な呼び出し構造を扱うのでなければ、大域脱出は必須と呼ぶほどの機能でもないということにはなります。 大域脱出を使う場合でも、通常はtry文を(特に実装の外部へ大域脱出の影響が及ばないように)忘れずに適切に設定する手間は依然として発生します。 そしてコールスタックのサイズがたかだか数個で済むような場合(このセクションでは説明のため、そのような例で説明していますが)、 実際のプログラミングではわざわざ大域脱出を持ち出す必要性は薄いと思います。
特に汎用で利用するためのglobalなライブラリ関数などを作成してユーザに提供する場合、 その関数のユーザにこのような大域脱出やtry文を使うことを強いる仕様にすべきではありません。 少なくともその窓口となる戻り値と引数ベースで、成功/失敗、エラーの種類、エラーメッセージを取得できるようにしておくのがおそらく一番健全で、 実際のライブラリでもそうなっている場合も多いと思います (数学用ライブラリのようなやむ得ない場合もありましょうが)。
この場合、戻り値は成功/失敗を表すbool値とするのがおそらく一番よいでしょう。 逆に言えば、戻り値の型はやむを得ない場合を除きあまり凝ったものにしない方がよいと思います。 例えば戻り値の型をenum型などにすることもできますが、その場合呼び出し元はswitch文(あるいはif-elifの連発)等で切り分けることになります。 これは単純なif文で判定するよりもほとんどの場合かなり手間になります。 関数のユーザはおそらくその成功/失敗程度であればif文で判定してくれるでしょうが(それすら面倒でしない場合もありましょうが)、 絶対に必要な状況を除いておそらく以下のようにわざわざswitch文(やそれに該当する記述)まで持ち出して判定してはくれないでしょう (何よりこれを漏れなくswitch文で網羅するには、ユーザはそのenum型の意味をすべて把握する必要があります)。
/* ライブラリ関数の実装 */
enum ErrorType {
Error_e_None
Error_e_CannotOpenFile
Error_e_SyntaxError
}
global parse( filename conststr ) ErrorType
{
/* something... */
return Error_e_None
}
/* 呼び出し側 */
switch parse( "test.txt" ) {
case Error_e_CannotOpenFile:
Rrk_print( "Cannot open file\n" )
case Error_e_SyntaxError:
Rrk_print( "Syntax error\n" )
default:
/* OK */
}
このような場合(つまりエラーの種類に応じて何か特別な処置をするのではなく単にエラーの種類を知りたい場合ですが)、 エラーメッセージを追加するための文字列型の引数を用意するのが、おそらく多くの場合でバランスがとれた方法のように思います。 引数で指定するため必ず強制されます(従って受け取り漏れの心配は通常ありません)し、たかがstringの変数を一つ用意する程度あればたいした手間でもないからです。
/* ライブラリ関数の実装 */
global parse( filename conststr, ermsg string ) bool
{
varia status bool
/* something... */
if !status {
ermsg &= "[Error]: Cannot open file.\n"
return false /* failure */
}
return true /* success */
}
/* 呼び出し側 */
varia ermsg string
if !parse( "test.txt", ermsg ){
/* error */
Rrk_print( ermsg \n )
}
一方、汎用でないプライベートな関数(global指定されていない関数)などを作成する場合であれば、(それらの関数の)インターフェース自体はそこまで堅牢にする必要はないかもしれません。 そのソースコードを見るのはその実装者に限定され、プライベートな関数は外のソースコードからもアクセスもできませんから、 インターフェースも好きにすればよいわけです。
今このプライベートな実装において、関数呼び出し階層が非常に深く、かつ複雑な再帰呼び出しを伴うような(たとえばパーザなどの実装では起こりがちですが)状況である場合を考えましょう。 関数呼び出し階層の深い部分でエラーが発生したとき、その時点で強制終了できる状況であれば簡単なのですが、 それはできず関数呼び出し階層の一番最上層までとりあえずエラーを伝播し、そこで適切な処置をしなければならない状況とします。
このときその階層に介在するほとんどの関数で戻り値チェックを行い、さらにそれに従ってエラー情報を呼び出し元の上層へたらい回し的に伝播させねばなりません。 これを漏れなく行うのは大変な手間になりますし、そもそも漏れがあった時点で最上層へのエラーの伝播が失敗します(つまりバグです)。 またコードも煩雑になりますので、このことで副次的なバグを生むかもしれません。 このような場合、大域脱出を使った方がむしろ確実です。
逆に言えば、そのような猛烈に複雑な呼び出し構造を扱うのでなければ、大域脱出は必須と呼ぶほどの機能でもないということにはなります。 大域脱出を使う場合でも、通常はtry文を(特に実装の外部へ大域脱出の影響が及ばないように)忘れずに適切に設定する手間は依然として発生します。 そしてコールスタックのサイズがたかだか数個で済むような場合(このセクションでは説明のため、そのような例で説明していますが)、 実際のプログラミングではわざわざ大域脱出を持ち出す必要性は薄いと思います。
以上でtry文の説明は完了となります。 通常はtry文まで把握すればこのセクション「大域脱出とtry文」の目的としては十分であり、 次の「setjmp文に関するその他の詳細」は読み飛ばして構いません。
setjmp文に関するその他の詳細
この項目はsetjmp文についてのさらなる詳細について参考までに説明しています。 通常はsetjmp文についてこれ以上知る必要はありません。 興味がある方のみお読み下さい。
参考:setjmp文が複数存在するケース
既に述べた通り、Rarakuではsetjmp文を複数書くこともできます。
まず一つの関数(あるいはグローバルスコープ)内にsetjmpが複数ある状況を考えます
(このような状況はあまり推奨されませんが)。
以下の例をご覧ください。
上記の例では一つの関数の中に複数のsetjmp文が記述されています。 この場合、この関数内において後に実行されたsetjmp文は、それより前に実行されたsetjmp文の効果を完全に上書きします。 つまり上記の例では実質2番目のsetjmpが最終的な効力を持ち、その後に実行されるlongjmp文の実行では2番目のsetjmp文の位置へ戻ることになります。
2番目のsetjmp文がまだ実行されていない段階でlongjmp文が呼ばれた場合も見ておきましょう。 例えば以下のような場合です。
上記の例ではlongjmp文の実行時点ではまだ2番目のsetjmp文は実行されておらず、1番目のsetjmpの効果がまだ生きています。 従って、このlongjmp文により1番目のsetjmp文の位置へ戻ることになります。
次に異なる関数(あるいはグローバルスコープ内)にsetjmpが複数ある状況を考えます。 以下の例をご覧ください。
上記の例のような状況では通常try文を使うところですが、 今回はsetjmpの説明であるため、敢えてその使用を避けて実体をむき出しにしています。
上記の例では、まずfunc1に入り1番目のsetjmpを実行します。 次にfunc2、func3へと入り、2番目のsetjmpを実行します。 しかし今回の場合、それらのsetjmpは同一関数内にあるわけではないため、 func3のsetjmpはfunc1のsetjmpの効果を上書きしません。 これらのsetjmpはネストされたような形で機能します。 最深部func4でのlongjmpの実行では、関数の呼び出し階層として(longjmpから)最も近いsetjmp(func3)へ戻る形になります。 これはちょうどtry文がネストできるような状況に(完全に同じではありませんが)似ています。
func3が自然終了すると、func3における全てのsetjmpの効力は消滅します。
ここでfunc2に戻り、そこにあるlongjmpが実行されますが、 今度のlongjmpでは残ったfunc1におけるsetjmpへ戻ることになります。
func1が自然終了すると、func1における全てのsetjmpの効力も消滅します。
最後はこのスタックが空になり、setjmpを一度も実行していない状態と同じになります。
この例の実行結果は以下のようになります。
通常setjmp文が一つの関数内で再度実行された場合、前回のsetjmpの効果を消去して新しい情報が上書きされます。 誤解のないようもっと正確に言えば、コールスタックにおける一つの要素で記憶できるsetjmpの位置の個数は、デフォルトでは一個だけということです。 一方、setjmp文に「*」を付けて実行した場合、そのような上書きはされず、一つの関数の中に複数の位置を保存できるようになります。 もっと正確に言えば、コールスタックにおける一つの要素で複数のsetjmpの位置を保存できるということです。
Close
以下の例をご覧ください。
function my_func()
{
setjmp; /* 1番目 */
/* something */
setjmp; /* 2番目 */
/* この間が無限ループ */
longjmp /* 2番目のsetjmpへ戻る */
}
上記の例では一つの関数の中に複数のsetjmp文が記述されています。 この場合、この関数内において後に実行されたsetjmp文は、それより前に実行されたsetjmp文の効果を完全に上書きします。 つまり上記の例では実質2番目のsetjmpが最終的な効力を持ち、その後に実行されるlongjmp文の実行では2番目のsetjmp文の位置へ戻ることになります。
2番目のsetjmp文がまだ実行されていない段階でlongjmp文が呼ばれた場合も見ておきましょう。 例えば以下のような場合です。
function my_func()
{
setjmp; /* 1番目 */
/* この間が無限ループ */
longjmp; /* 1番目のsetjmpへ戻る */
/* something */
setjmp; /* 2番目 */
}
上記の例ではlongjmp文の実行時点ではまだ2番目のsetjmp文は実行されておらず、1番目のsetjmpの効果がまだ生きています。 従って、このlongjmp文により1番目のsetjmp文の位置へ戻ることになります。
次に異なる関数(あるいはグローバルスコープ内)にsetjmpが複数ある状況を考えます。 以下の例をご覧ください。
function func4()
{
Rrk_print( "func4 : longjmp\n" )
longjmp /* func3におけるsetjmpへ戻る */
}
function func3()
{
Rrk_print( "func3 : begin\n" )
varia once = 0 setjmp if once ? 0 : ++once { /* func3におけるsetjmp(2番目) */
func4()
}
Rrk_print( "func3 : end\n" )
}
function func2()
{
func3()
Rrk_print( "func2 : longjmp\n" )
longjmp /* func1におけるsetjmpへ戻る */
}
function func1()
{
Rrk_print( "func1 : begin\n" )
varia once = 0 setjmp if once ? 0 : ++once { /* func1におけるsetjmp(1番目) */
func2()
}
Rrk_print( "func1 : end\n" )
}
func1() /* entry point */
上記の例のような状況では通常try文を使うところですが、 今回はsetjmpの説明であるため、敢えてその使用を避けて実体をむき出しにしています。
上記の例では、まずfunc1に入り1番目のsetjmpを実行します。 次にfunc2、func3へと入り、2番目のsetjmpを実行します。 しかし今回の場合、それらのsetjmpは同一関数内にあるわけではないため、 func3のsetjmpはfunc1のsetjmpの効果を上書きしません。 これらのsetjmpはネストされたような形で機能します。 最深部func4でのlongjmpの実行では、関数の呼び出し階層として(longjmpから)最も近いsetjmp(func3)へ戻る形になります。 これはちょうどtry文がネストできるような状況に(完全に同じではありませんが)似ています。
正確に言えば、これらのsetjmpの情報はスタックで積まれたような形で保持されます。
ただし一つの関数またはグローバルスコープににおいてプッシュできるsetjmpの情報は最大1つまでです。
(同一関数内に複数のsetjmp文がある場合、2番目のsetjmpの情報が追加的にプッシュされることはなく、替わりに既にプッシュされている1番目のsetjmpの情報を単に上書きします)。
またlongjmp文のより正確な挙動は、このスタックのトップにあるsetjmpの情報を参照し、それが示す位置へ戻るというものになります。
func3が自然終了すると、func3における全てのsetjmpの効力は消滅します。
正確に言えば、このときsetjmpが詰まれたスタックをポップし、func3に関するsetjmpの情報だけを破棄します。
これにより、今度はfunc1におけるsetjmpの情報がスタックのトップに残ることになります。
ここでfunc2に戻り、そこにあるlongjmpが実行されますが、 今度のlongjmpでは残ったfunc1におけるsetjmpへ戻ることになります。
func1が自然終了すると、func1における全てのsetjmpの効力も消滅します。
正確に言えば、このときsetjmpが詰まれたスタックをポップし、func1に関するsetjmpの情報だけを破棄します。
最後はこのスタックが空になり、setjmpを一度も実行していない状態と同じになります。
この例の実行結果は以下のようになります。
func1 : begin
func3 : begin
func4 : longjmp
func3 : end
func2 : longjmp
func1 : end
通常setjmp文が一つの関数内で再度実行された場合、前回のsetjmpの効果を消去して新しい情報が上書きされます。 誤解のないようもっと正確に言えば、コールスタックにおける一つの要素で記憶できるsetjmpの位置の個数は、デフォルトでは一個だけということです。 一方、setjmp文に「*」を付けて実行した場合、そのような上書きはされず、一つの関数の中に複数の位置を保存できるようになります。 もっと正確に言えば、コールスタックにおける一つの要素で複数のsetjmpの位置を保存できるということです。
Close
参考:unsetjmp文
Rarakuではsetjmpの効果を途中で明示的に消すためのunsetjmp文が用意されています。
unsetjmp文はこの関数内でsetjmpが実行されていた場合、最後に実行されたsetjmpの効果を打ち消します。
(ただし別の関数で実行されたsetjmpの効果は打ち消しません)。
より正確にはコールスタックにおける最後の要素に該当する関数(つまり今実行中の関数)のsetjmp文のうち、
最後に設定されたものの効果のみを打ち消します。
関数内にsetjmpが一度も実行されていない場合や、既に効果を打ち消した後でsetjmpの効果が一つも残っていない状態でunsetjmp文を実行した場合、 何も起こりません。
Close
関数内にsetjmpが一度も実行されていない場合や、既に効果を打ち消した後でsetjmpの効果が一つも残っていない状態でunsetjmp文を実行した場合、 何も起こりません。
unsetjmpを実行した後、またsetjmpを実行することもできます。
そこからまたunsetjmpを実行すれば再びsetjmpの効果が打ち消されることになります。
Close
言語仕様の背景: try文が実装されるまでの道のり1: 最もシンプルな状況
実は、try文はRarakuの開発過程の中でも比較的後期に実現した文です。
一方、setjmp/longjmp/unsetjmp文はRarakuの開発過程の中でも比較的初期には実現していた文になります。
setjmp/longjmp/unsetjmp文で大域脱出については実現できるのですが、
最終的にいわゆるC++/Javaのtry-catchのような機構を導入するかどうか、また導入するとして、throwに相当するものを実行したとき
その引数としてどのような情報を渡すか(C++/Javaではそれは「例外」と呼ばれますが)をかなり長い間仕様として決めかねていたため
(結局文字列限定とすることに落ち着いたわけですが)、
try文の導入は随分後になったという経緯があります。
ここでは最後のおまけとして、setjmp/longjmp/unsetjmp文から出発して、最終的にtry文をどう実装したかについて その「道のり」のようなものを話したいと思います(つまりチュートリアルでも言語仕様の話でもなく、ただの趣味の雑談です)。
Rarakuのtry文のような機能をsetjmp/longjmp文で実現するには、 try文が一つだけの最もシンプルな状況ならば、とりあえず以下のようにすればよいことは既に述べました。
我々も(try文を導入しなくても)とりあえずこれで十分ではないかと考えていた時期もあります。 「longjmp:ex」の部分を書くのが(一旦exに格納しないといけない分)ちょっと面倒で 「varia once = 0 setjmp:ex if once ? 0 : ++once { /* try */」の行を書くのは猛烈に面倒ですが、 それでも以下のような関数を作れば多少短く書けます。
しかしそれでもonceという処理の本質とは無関係な変数を導入しなければなりませんし、しかもそれがユーザから見えてしまっています。 さらにこれが一つのブロック内で二つ使われる場合に備えるなら、once2と言うように名前を変えるか あるいは以下のようにそれぞれのonceをブロックで囲わなければなりません。
Close
ここでは最後のおまけとして、setjmp/longjmp/unsetjmp文から出発して、最終的にtry文をどう実装したかについて その「道のり」のようなものを話したいと思います(つまりチュートリアルでも言語仕様の話でもなく、ただの趣味の雑談です)。
Rarakuのtry文のような機能をsetjmp/longjmp文で実現するには、 try文が一つだけの最もシンプルな状況ならば、とりあえず以下のようにすればよいことは既に述べました。
function func3()
{
varia result = false
if !result {
/* ここで何らかのエラーが発生したものと仮定する */
const ex = "func3 : error"
longjmp:ex /* func1におけるsetjmpの位置に一気に戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia ex string
varia once = 0 setjmp:ex if once ? 0 : ++once { /* try */
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error [" ex "].\n" )
}
}
func1() /* entry point */
我々も(try文を導入しなくても)とりあえずこれで十分ではないかと考えていた時期もあります。 「longjmp:ex」の部分を書くのが(一旦exに格納しないといけない分)ちょっと面倒で 「varia once = 0 setjmp:ex if once ? 0 : ++once { /* try */」の行を書くのは猛烈に面倒ですが、 それでも以下のような関数を作れば多少短く書けます。
function Raise( ex conststr )
{
longjmp:ex
}
function Try( refer once int ) int
{
return once ? 0 : ++once
}
function func3()
{
varia result = false
if !result {
/* ここで何らかのエラーが発生したものと仮定する */
Raise( "func3 : error" ) /* func1におけるsetjmpの位置に一気に戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia ex string
varia once = 0 setjmp:ex if Try(once) { /* try */
func2()
} else { /* catch error */
Rrk_print( "func1 : catch error [" ex "].\n" )
}
}
func1() /* entry point */
しかしそれでもonceという処理の本質とは無関係な変数を導入しなければなりませんし、しかもそれがユーザから見えてしまっています。 さらにこれが一つのブロック内で二つ使われる場合に備えるなら、once2と言うように名前を変えるか あるいは以下のようにそれぞれのonceをブロックで囲わなければなりません。
function Raise( ex conststr )
{
longjmp:ex
}
function Try( refer once int ) int
{
return once ? 0 : ++once
}
function func1()
{
varia ex string
/* 1回目 */
{
varia once = 0 setjmp:ex if Try(once) { /* try */
Raise( "error" )
} else { /* catch error */
Rrk_print( "func1 : catch error [" ex "].\n" )
}
}
/* 2回目 */
{
varia once = 0 setjmp:ex if Try(once) { /* try */
Raise( "error" )
} else { /* catch error */
Rrk_print( "func1 : catch error [" ex "].\n" )
}
}
}
func1() /* entry point */
Close
言語仕様の背景: try文が実装されるまでの道のり2: 連鎖的に例外を投げるような状況
C++やJavaにおけるtry-catch文では、throwによりcatch文のブロック内を実行中に、さらに連鎖的にthrowして上層のtry-catch文へ戻るといったようなことができます。
Rarakuにおけるsetjmp文、longjmp文でもそのような状況を実現すべく、以下のように記述したとします
(ただしこの記述では正しく動作しません)。
上記の例では、まずfunc1、func2、func3、func4の順に入っていき、func4の時点で最初のlongjmpの実行が行われます。 これによりfunc3におけるsetjmpへ移動し、すぐ下のelseブロックが実行されます。 このelseブロックの中でさらにlongjmpを実行し、今度はfunc1におけるsetjmpへ移動したいところですが、 残念ながらこの例ではうまくいきません。
func3におけるsetjmpはまだ消滅しておらず生きています。 そのため、func3においてlongjmpを実行しても再びfunc3におけるsetjmpへ戻ってしまい、 無限ループとなってしまいます。
このsetjmpの効果を消した上でlongjmpを実行すればよさそうですが、setjmpの効果は通常、その関数を自然終了するまで消滅しません。 これを強制的に打ち切るため、unsetjmp文をfunc3におけるelseブロックの最初に置きましょう。 これで、このelseブロックにあるlongjmpからfunc1におけるsetjmpへジャンプすることができます。
この修正を加えた例は以下になります。
Close
function func4()
{
Rrk_print( "func4 : longjmp\n" )
longjmp /* func3におけるsetjmpへ戻る */
}
function func3()
{
varia once = 0 setjmp if once ? 0 : ++once { /* func3におけるsetjmp(2番目) */
func4()
} else { /* catch */
Rrk_print( "func3: catch error\n" )
longjmp /* 本当はfunc1におけるsetjmpへ戻りたいが、func3のsetjmpへ戻ってしまい無限ループとなる. */
}
}
function func2()
{
func3()
}
function func1()
{
varia once = 0 setjmp if once ? 0 : ++once { /* func1におけるsetjmp(1番目) */
func2()
} else { /* catch */
Rrk_print( "func1: catch error\n" )
}
}
//func1()
上記の例では、まずfunc1、func2、func3、func4の順に入っていき、func4の時点で最初のlongjmpの実行が行われます。 これによりfunc3におけるsetjmpへ移動し、すぐ下のelseブロックが実行されます。 このelseブロックの中でさらにlongjmpを実行し、今度はfunc1におけるsetjmpへ移動したいところですが、 残念ながらこの例ではうまくいきません。
func3におけるsetjmpはまだ消滅しておらず生きています。 そのため、func3においてlongjmpを実行しても再びfunc3におけるsetjmpへ戻ってしまい、 無限ループとなってしまいます。
このsetjmpの効果を消した上でlongjmpを実行すればよさそうですが、setjmpの効果は通常、その関数を自然終了するまで消滅しません。 これを強制的に打ち切るため、unsetjmp文をfunc3におけるelseブロックの最初に置きましょう。 これで、このelseブロックにあるlongjmpからfunc1におけるsetjmpへジャンプすることができます。
この修正を加えた例は以下になります。
function func4()
{
Rrk_print( "func4 : longjmp\n" )
longjmp /* func3におけるsetjmpへ戻る */
}
function func3()
{
varia once = 0 setjmp if once ? 0 : ++once { /* func3におけるsetjmp(2番目) */
func4()
} else { /* catch */
unsetjmp /* func3におけるsetjmpの効果を打ち消す */
Rrk_print( "func3: catch error\n" )
longjmp /* func1におけるsetjmpへ戻る */
}
}
function func2()
{
func3()
}
function func1()
{
varia once = 0 setjmp if once ? 0 : ++once { /* func1におけるsetjmp(1番目) */
func2()
} else { /* catch */
Rrk_print( "func1: catch error\n" )
}
}
func1()
Close
言語仕様の背景: try文が実装されるまでの道のり3: 一つの関数内でネストするような状況
しかし、ここまで来てもまだ問題があります。
この実装ではこれを一つの関数内においてネストすることができません。
つまりもしtry文を使ったならば以下のように記述される状況です。
既に説明したとおり、try文におけるsetjmpの効果はelseブロック内に到達した時点、あるいはtryブロックが完全に終わった後に打ち消されます。 そもそもこの必要がある理由は上記のようなtry文のネストに対応するためです (このようなtry文のネストを絶対にしないと取り決めているなら、効果を打ち消す必要はありませんが)。
ではこの状況をtry文を使わず書くにはどうすればよいでしょうか?
単なるsetjmpでは、一つの関数で同時に一つのsetjmp位置しかセットできません。 ネストするには一つの関数で同時に複数のsetjmp位置をセットする必要がありますから、「setjmp*」が必須になります。 また、ifブロックの最後とelseブロックの最初にunsetjmpを忘れずに書かなければなりません。
以下はRarakuコンパイラ内部で行っている実装の真の構造にかなり近いものです。
ここまでやればネストには対応できます。 ただこれはもはや煩雑になり過ぎ、嫌になるレベルでしょう。
また最後にトドメを指すような話ですが、ここまでやってもgoto文、continue文、break文、fallthrough文の問題のある記述に対するチェックはできません。 これらに対するチェックも万全なものとするためには、やはりコンパイラレベルでのtry文のサポートが必要になるわけです。 観念してtry文を使いましょう。
おしまい。
Close
function func3()
{
varia result = false
if !result {
/* func1における「内側のtry文」の位置に一気に戻る */
Rrk_raise( "error from func3" )
}
}
function func2()
{
func3()
}
function func1()
{
varia ex string
try ex { /* 外側のtry文 */
varia ex string
try ex { /* 内側のtry文 */
func2()
} else { /* catch error */
/***
* このelseブロック内では「内側のtry文」のsetjmpの効果は消滅している.
* ただし「外側のtry文」のsetjmpの効果はまだ残っている.
*/
/* 再びRrk_raiseし、今度は「外側のtry文」へジャンプする */
Rrk_print( "func1 : inner : catch error [" ex "].\n" )
Rrk_raise( ex )
}
} else {
Rrk_print( "func1 : outer : catch error [" ex "].\n" )
}
}
func1() /* entry point */
既に説明したとおり、try文におけるsetjmpの効果はelseブロック内に到達した時点、あるいはtryブロックが完全に終わった後に打ち消されます。 そもそもこの必要がある理由は上記のようなtry文のネストに対応するためです (このようなtry文のネストを絶対にしないと取り決めているなら、効果を打ち消す必要はありませんが)。
ではこの状況をtry文を使わず書くにはどうすればよいでしょうか?
単なるsetjmpでは、一つの関数で同時に一つのsetjmp位置しかセットできません。 ネストするには一つの関数で同時に複数のsetjmp位置をセットする必要がありますから、「setjmp*」が必須になります。 また、ifブロックの最後とelseブロックの最初にunsetjmpを忘れずに書かなければなりません。
以下はRarakuコンパイラ内部で行っている実装の真の構造にかなり近いものです。
function func3()
{
varia result = false
if !result {
const ex = "error from func3"
longjmp:ex
}
}
function func2()
{
func3()
}
function func1()
{
varia ex string
/***
* もしも「try文」を使えば、Rarakuパーザによりtry文の部分がこれに該当する処理に自動的に展開される。
* ただしパーザ内部ではユーザが使用不可な名前(@_try_once_)が使われる.
* 「@」を識別子として使うことは(ユーザは)許可されないため、名前が衝突することはあり得ないことになる.
*/
{ varia _try_once_ = 0 setjmp*:ex if _try_once_ ? 0 : ++_try_once_ { /* 外側のtry文に相当 */
varia ex string
{ varia _try_once_ = 0 setjmp*:ex if _try_once_ ? 0 : ++_try_once_ { /* 内側のtry文に相当 */
func2()
unsetjmp /* tryブロックの終わりにつきunsetjmp(もしも「try文」を使えばこれはRarakuパーザが自動的に挿入する) */
} else { /* catch error */
unsetjmp /* elseブロックの始まりにつきunsetjmp(もしも「longjmp*」を使えばこれはRarakuVMが自動的に挿入する) */
/***
* このelseブロック内では「内側のtry文」のsetjmpの効果は消滅している.
* ただし「外側のtry文」のsetjmpの効果はまだ残っている.
*/
/* 再びlongjmpし、今度は「外側のtry文」へジャンプする */
Rrk_print( "func1 : inner : catch error [" ex "].\n" )
longjmp:ex
}}
unsetjmp /* tryブロックの終わりにつきunsetjmp(もしも「try文」を使えばこれはRarakuパーザが自動的に挿入する) */
} else {
unsetjmp /* elseブロックの始まりにつきunsetjmp(もしも「longjmp*」を使えばこれはRarakuVMが自動的に挿入する) */
Rrk_print( "func1 : outer : catch error [" ex "].\n" )
}}
}
func1() /* entry point */
ここまでやればネストには対応できます。 ただこれはもはや煩雑になり過ぎ、嫌になるレベルでしょう。
また最後にトドメを指すような話ですが、ここまでやってもgoto文、continue文、break文、fallthrough文の問題のある記述に対するチェックはできません。 これらに対するチェックも万全なものとするためには、やはりコンパイラレベルでのtry文のサポートが必要になるわけです。 観念してtry文を使いましょう。
おしまい。
Close
目次に戻る
ジェネリクス
ジェネリクスの必要性とその基本的な記法
ジェネリクスとは、型をプレースホルダとして関数の宣言や定義を行ったものです。 この型は実際の使用時(すなわち関数呼び出し時や参照時)に具体的に決定され、関数自体も一意に確定します。 確定した関数の実体は自動的に作成されます。
ジェネリクスがどのような場合に役に立ち、どのように記述するのかを示すため、 まずは以下のようにint型とreal型を使った二つのswap関数を考えましょう。
function swap_int( refer x int, refer y int )
{
const tmp int = x
x = y
y = tmp
}
function swap_real( refer x real, refer y real )
{
const tmp real = x
x = y
y = tmp
}
varia i=10, j=20
Rrk_print( \=i \, \=j \n )
swap_int( i, j )
Rrk_print( \=i \, \=j \n )
varia p=10.5, q=20.5
Rrk_print( \=p \, \=q \n )
swap_real( p, q )
Rrk_print( \=p \, \=q \n )
上記でswap_int関数とswap_real関数は引数や一時変数tmpの型こそ違いますが、内部で行っている処理は基本的には同じです。 このような冗長性のある記述を、対応する型の数だけ用意するのは通常は避けた方がプログラムの管理は容易になります。 そこでジェネリクスを使います。
Rarakuのジェネリクスは「function 関数名 ^<ジェネリクス型名> ( 引数の羅列 )」といった書式になります。
C++に慣れている方は、「<」と「>」ではなく「^<」と「>」であるという点にもご注意ください。
例えばswap関数の場合以下のようになります。
function swap^<T>( refer x T, refer y T )
{
const tmp T = x
x = y
y = tmp
}
varia i=10, j=20
Rrk_print( \=i \, \=j \n )
swap( i, j )
Rrk_print( \=i \, \=j \n )
varia p=10.5, q=20.5
Rrk_print( \=p \, \=q \n )
swap( p, q )
Rrk_print( \=p \, \=q \n )
上記ではswap関数がジェネリクスとなり、Tがジェネリクス型となります。 このswap関数においては、型がTという名前で抽象化されて記述されています。 このように抽象化することでswap関数の定義の記述を一つだけに留めることができます。
ただし上記の実装は非集成体の場合であれば問題ありませんが、
集成体の場合ではまだ問題があります。
ダブルモディファイアのセクションでも述べましたが、
refer付きの集成体仮引数の場合、実引数と仮引数のダブルモディファイアが完全に一致していなければなりません。
上記の実装では単に「refer」としていますから「refer immut immut」と同義であり、
つまり集成体の場合は「immut immut」で指定された実引数しか指定できないことになります。
この問題を解決し、非集成体と集成体のいずれの場合でも問題ないswapの完全版を実装するには モディファイアのジェネリクスとauto文が必要になります。 これについては後で述べます。
この問題を解決し、非集成体と集成体のいずれの場合でも問題ないswapの完全版を実装するには モディファイアのジェネリクスとauto文が必要になります。 これについては後で述べます。
ジェネリクス型Tが具体的にどんな型であるのかは定義の時点ではまだ確定しません。 ただし少なくともTの部分にはすべて同じ型が当てはまることは確定しています。 つまり x、y、tmpは同じ型でなければなりません。
ジェネリクス型Tが具体的に決まるのは、実際の関数呼び出し時の時点です。 例えばswap( i, j )では引数にint型を与えているため、そこからTがint型と型推論され、 int型の引数を持つswap関数が裏で一つ自動的に生成されることになり、それが呼び出されます。
すぐ下のswap( p, q )では引数にreal型を与えているため、今度はそこからTがreal型と型推論され、 real型の引数を持つswap関数が裏で一つ自動的に生成されることになり、それが呼び出されます。
このように同じswap関数でも与える型が異なる場合、その型毎に複数の実体が作成されます。 上の例では一つのswap関数についてトータルで二つの実体が作成されることになります。 このような実体を特にインスタンシエーション(instantiation)と呼ぶことがあります。 またこのような実体を作成することをインスタンシエートする(instantiate)と呼びます。
ジェネリクス内で記述可能なコード
ジェネリクスの定義の時点ではまだ型が具体的に確定していないため、 何だかの具体的な型に依存するような記述ができない場合があります。 これは後述する型制約の指定によって調整が可能ですが、ここではまず、そのような型制約を一切指定しなかった場合において、 ジェネリクス内に記述可能なケースと不可能なケースの例をいくつか挙げます。
以下のようにconst/vaira文の宣言における型指定で、ジェネリクス型を用いるのは問題ありません。 またジェネリクス型の変数や定数への代入も(ジェネリクス型が同じであれば)問題ありません。
function func^<T>( refer x T, refer y T )
{
const tmp T = x
}
varia文の場合初期化を省略することもできます。
function func^<T>( x T )
{
varia tmp T
}
この場合、インスタンシエートした時点でのTの型よりデフォルト値が推定され、tmpの初期値が自動的に割り当てられます。
以下のようにジェネリクス型をstatic指定することも可能です。
function func^<T>( x T )
{
static varia tmp T
}
この場合、インスタンシエートされた関数の個数分、static変数も割り当てられることになります。
以下のようにジェネリクス型の配列を引数の型として宣言することも可能です。
function func^<T>( tight ary T[], e T )
{
ary[0] = e
}
varia ary int[3] = [ 10, 20, 30 ]
func( ary, ary[2] )
また以下のようにジェネリクス型の配列を戻り値の型として宣言することもできます。
function func^<T>( tight ary T[], e T ) tight T[]
{
ary[0] = e
return ary
}
varia ary int[3] = [ 10, 20, 30 ]
tight ary2 = func( ary, ary[2] )
また以下のようにジェネリクス型の配列を内部のローカル変数として宣言することもでき、 varia文の場合、配列の初期化子を省略することもできます。
function func^<T>( val T )
{
varia tmp_ary T[]
}
varia ival int
func( ival )
この場合、インスタンシエートした時点でのTの型よりその配列のデフォルト値が推定され、tmp_aryの初期値が自動的に割り当てられます
以下のようにジェネリクス型同士で「==」または「!=」演算子を用いた比較をすることができます。
function func1^<T>( t1 T, t2 T ) bool
{
return t1 == t2
}
function func2^<T>( t1 T, t2 T ) bool
{
return t1 != t2
}
しかし以下のようにジェネリクス型同士で大小比較(すなわち「>」「>=」「<」「<=」演算子を用いた比較)をすることはデフォルトではできません。
function func1^<T>( t1 T, t2 T ) bool
{
return t1 <= t2 /* デフォルトではコンパイルエラー */
}
function func2^<T>( t1 T, t2 T ) bool
{
return t1 > t2 /* デフォルトではコンパイルエラー */
}
これを許可するにはcomparable制約を指定する必要があります。
制約については後述します。
以下のようにジェネリクス型に対して、文字列連結演算子を使用することができます。
function func1^<T>( t1 T )
{
varia str string = "hello"
str &= t1
Rrk_print( \=str\n )
}
func1( 3 )
Rarakuではすべての型は何らかの文字列に変換できるため、これは問題ありません。
ジェネリクス型の複数指定
以下のように複数のジェネリクス型をカンマで区切って指定することが可能です。
function func^<S, T>( s S, t T )
{
varia tmp_s S = s
varia tmp_t T = t
}
func( 1, 2.5 )
上記ではジェネリクス型としてまずSとTが使用されることが「^<…>」内で予告され、 実際それは第1引数と第2引数、あるいは定義内の一時変数tmp_sとtmp_tの宣言において使用されています。
func関数呼び出し時においては第1引数では1(int型)、第2引数では2.5(real型)が指定されているため、 このときSはint、Tはrealとしてインスタンシエートされます。
明示的なインスタンシエート
ここまでの説明は全て、関数呼び出し時に指定された引数の型より自動的にインスタンシエートを行うというものでした。 一方、引数の型からではなくプログラマが明示的にその型を指定してインスタンシエートすることもできます。 これを明示的なインスタンシエートと呼びます。
明示的なインスタンシエートを行うには関数識別子の後ろにインスタンシエート演算子「^<」と「>」を付加します。 例えば以下の通りです。
function func1^<T>( t1 T )
{
Rrk_print( \=t1\n )
}
func1^<int>( 3 ) /* int型で明示的にインスタンシエート */
上記ではジェネリクスfunc1をint型で明示的にインスタンシエートしたものを関数呼び出ししています。
実引数の型よりも明示的なインスタンシエートによる型指定の方が優先され、 その上で実引数の型がチェックされます。 例えば以下では、明示的なインスタンシエートにより最終的にはint型の引数をとる関数へインスタンシエートされますが、 実引数としては実数(3.5)を与えているため、型の不一致でコンパイルエラーとなります。
function func1^<T>( t1 T )
{
Rrk_print( \=t1\n )
}
func1^<int>( 3.5 ) /* コンパイルエラー : int型で明示的にインスタンシエートいる所に実数3.5を指定している */
複数のジェネリクス型を持つジェネリクスに対して明示的なインスタンシエートを行う場合、これらの型はカンマで区切って指定します。 例えば以下の通りです。
function func^<S, T>( s S, t T )
{
varia tmp_s S = s
varia tmp_t T = t
}
func^<int, real>( 1, 2.5 )
上記では、ジェネリクス型S、Tの二つに対して、明示的なインスタンシエートを行うため、intとrealの二つをカンマで区切って指定しています。
言語仕様の背景: インスタンシエートの記号が ^< となっている理由
C++などではインスタンシエートの記号として < と > が使われていますが、Rarakuでは ^< と > となっています。
一つ目の理由はインスタンシエートの開始記号として < を採用すると、Rarakuパーザ(コンパイラの構文解析フェーズ)において
大小比較演算子 < との区別が難しくなるからです。
例えば以下のコードをご覧下さい。
今仮にインスタンシエートの開始記号として < を採用したとしますと、上記のコードは構文解析フェーズにおいては二通りに解釈できてしまいます。 すなわち「X < Y」と「Z > F」 という二つの条件が並んでいるという解釈と、 インスタンシエート「X <Y, Z>」 と識別子Fが並んでいるという解釈です。 しかも不幸なことに、両者は構造(構文)だけを見ても共通性のない全く異質な解釈です。
このどちらであるかを確定させるには、X, Y, Z, Fの型がそれぞれ何であるのか(特にXが関数型であるのか否か)を解析しなければなりませんが、 それは構文解析フェーズを経た後の意味解析フェーズの仕事になります (構文解析フェーズの目的は一旦トークンの(式や文としての)構造を確定させることですので、 個々の要素の意味について「詳細には」解析することができません)。
とはいえ多くの場合、Xが関数であることは構文解析フェーズの段階でも判明します。 例えば上記のトークン列が現れるより前に、Xが既に関数として定義や宣言されている場合です。 しかしRarakuでは特にstatic関数の定義を、その関数呼び出しよりも後に定義することができ、 しかも前方にそれらの宣言も書かないルールになっています(この点がC++などとは異なっています)。 そのため、上記で識別子Xが始めて出現した場合、それが(後方で現れるであろう)static関数であるのか、 それとも単にユーザが文字を打ち間違えて存在しない識別子を書いてしまったのか (エラーとして処理すべきケースなのか)の判断が(構文解析フェーズの段階では)判断がつきません。
二つ目の理由はインスタンシエートの開始記号として < を採用すると、外部のツールでの処理(例えばテキストエディタの(括弧の類いの閉じ忘れをチェックするための)ハイライト機能など)において 大小比較演算子 < との区別が難しくなるからです。 既に述べたようにその処理はかなりややこしいものになりますが、 外部のツールでそこまでの処理をさせるのは、多くの場合、現実的ではないと思われます。
尚、Rarakuではインスタンシエートとしての記号ではなくジェネリクスの宣言部における記号でも、 ^< と > を使うものとしています。 将来的にその宣言部の内部に大小比較演算子 < を導入するようなことでもない限り、 この部分においては上記で述べたような混同が起きることはないのですが、 インスタンシエートとしての記号との統一性をとるため、ジェネリクスの宣言部における記号もそれと同じにしています。
Close
X < Y, Z > F
今仮にインスタンシエートの開始記号として < を採用したとしますと、上記のコードは構文解析フェーズにおいては二通りに解釈できてしまいます。 すなわち「X < Y」と「Z > F」 という二つの条件が並んでいるという解釈と、 インスタンシエート「X <Y, Z>」 と識別子Fが並んでいるという解釈です。 しかも不幸なことに、両者は構造(構文)だけを見ても共通性のない全く異質な解釈です。
このどちらであるかを確定させるには、X, Y, Z, Fの型がそれぞれ何であるのか(特にXが関数型であるのか否か)を解析しなければなりませんが、 それは構文解析フェーズを経た後の意味解析フェーズの仕事になります (構文解析フェーズの目的は一旦トークンの(式や文としての)構造を確定させることですので、 個々の要素の意味について「詳細には」解析することができません)。
とはいえ多くの場合、Xが関数であることは構文解析フェーズの段階でも判明します。 例えば上記のトークン列が現れるより前に、Xが既に関数として定義や宣言されている場合です。 しかしRarakuでは特にstatic関数の定義を、その関数呼び出しよりも後に定義することができ、 しかも前方にそれらの宣言も書かないルールになっています(この点がC++などとは異なっています)。 そのため、上記で識別子Xが始めて出現した場合、それが(後方で現れるであろう)static関数であるのか、 それとも単にユーザが文字を打ち間違えて存在しない識別子を書いてしまったのか (エラーとして処理すべきケースなのか)の判断が(構文解析フェーズの段階では)判断がつきません。
もっと凝ったコンパイラの場合、1つのファイルを二周することで構文解析フェーズを完成させる方法をとっているかもしれません。
この方法であれば、構文解析フェーズでもXが関数であることは(仮にその関数の定義が後方にあったとしても)確定できます。
しかしその場合、意味解析フェーズも合わせると、1つのファイルを実質三周していることになり、
今度は効率が悪くなるためRarakuではそのような方法もとっていません。
また例えばGo言語では、そもそもインスタンシエートの記号として < と > ではなく [ と ] を採用しています。 このような記号を採用した詳しい経緯までは知りませんが、大小比較演算子と混同する問題を回避したいという思惑も多分あるのでしょう。 ただしこの記号の場合、今度は配列(スライス)の記号と混同しそうですが、(大小比較演算子の場合とは異なり) 何かを囲うという構造(構文)は(インスタンシエートでも配列でも)共通しているので、構文解析フェーズでは問題ないということなのでしょう。
また例えばGo言語では、そもそもインスタンシエートの記号として < と > ではなく [ と ] を採用しています。 このような記号を採用した詳しい経緯までは知りませんが、大小比較演算子と混同する問題を回避したいという思惑も多分あるのでしょう。 ただしこの記号の場合、今度は配列(スライス)の記号と混同しそうですが、(大小比較演算子の場合とは異なり) 何かを囲うという構造(構文)は(インスタンシエートでも配列でも)共通しているので、構文解析フェーズでは問題ないということなのでしょう。
二つ目の理由はインスタンシエートの開始記号として < を採用すると、外部のツールでの処理(例えばテキストエディタの(括弧の類いの閉じ忘れをチェックするための)ハイライト機能など)において 大小比較演算子 < との区別が難しくなるからです。 既に述べたようにその処理はかなりややこしいものになりますが、 外部のツールでそこまでの処理をさせるのは、多くの場合、現実的ではないと思われます。
尚、Rarakuではインスタンシエートとしての記号ではなくジェネリクスの宣言部における記号でも、 ^< と > を使うものとしています。 将来的にその宣言部の内部に大小比較演算子 < を導入するようなことでもない限り、 この部分においては上記で述べたような混同が起きることはないのですが、 インスタンシエートとしての記号との統一性をとるため、ジェネリクスの宣言部における記号もそれと同じにしています。
Close
すべての型を明示的に指定するのではなく、最初の何個かだけを指定することもできます。 その場合、(明示的に型指定されていない)残りの型については関数呼び出し時の引数の型から自動的に判断されます。 この場合もやはり実引数の型よりも明示的なインスタンシエートによる型指定の方が優先されます。 例えば以下の通りです。
function func^<S, T, U>( s S, t T, u U )
{
varia tmp_s S = s
varia tmp_t T = t
varia tmp_u U = u
}
func^<int>( 1, 2.5, "abc" )
上記では、ジェネリクス型S、Tのうち、最初のSについてのみ明示的な指定を行い、 残りのTとUについては実際に与えられた引数「2.5」「"abc"」よりreal型、conststr型と判断され、 これら二つの情報を掛け合わせて最終的に「<int, real>」という指定と全く同じ形でインスタンシエートされます。
現時点ではN番目の型を明示的に指定をした場合、それより前(1番目からN-1番目まで)もすべて明示的に指定する必要があります。
例えば上記では、Tの型を明示的に指定したならば「func^<int,real>(1, 2.5, "abc")」などのようにSの型も明示的に指定しなければなりません。
またUの型を明示的に指定したならば「func^<int,real,conststr>(1, 2.5, "abc")」などのようにSの型とTの型も明示的に指定しなければなりません。
尚、func^<>(1, 2.5, "abc")」などのように明示的なインスタンシエートの引数を空にした記述をすることはできません。 実引数のみで型を判断したい場合は、単に「func(1, 2.5, "abc")」などのように記述します。
尚、func^<>(1, 2.5, "abc")」などのように明示的なインスタンシエートの引数を空にした記述をすることはできません。 実引数のみで型を判断したい場合は、単に「func(1, 2.5, "abc")」などのように記述します。
ジェネリクスの定義の中に別のジェネリクスの明示的なインスタンシエートを記述することもできます。 例えば以下の通りです。
function func1^<T>( t T )->
{
Rrk_print( \=t\n )
return t
}
function func2^<T>( t T )->
{
return func1^<T>( t )
}
func2^<int>( 5 )
上記ではジェネリクスfunc2の内部において、ジェネリクスfunc1を型Tで明示的にインスタンシエートしており、 しかもそこで指定されている型Tはジェネリクス型となっています。 このような場合、func2の定義の時点ではその内部のfunc1の型は確定しません。 一番最後にあるfunc2のインスタンシエートが行われたタイミングで、その内部のfunc1の型も確定します。 この例の場合、func2におけるTがint型であるという情報が内部へと連鎖的に適用され、func1のTも最終的にint型としてインスタンシエートされることになります。
上記では型隠蔽演算子「->」も同時に使用しています。
この場合、最終的にインスタンシエートされるタイミングにおいて、戻り値の型推論が行われます。
ジェネリクスにおける制約(constraint)
既に述べたようにジェネリクスの定義において、大小関係の比較演算子(「>」「>=」または「<」「<=」)の使用はデフォルトでは許可されません。 ただし適切な制約(constraint)を指定することでこれらの演算子の使用を許可することができます。 ジェネリクス型における制約の指定は「^< ジェネリクス型名 コロン(:) 制約名 >」といった書式になります。 大小関係の比較演算子を用いるには、制約名としてcomparableを指定します。 例えば以下のようになります。
function func1^<T:comparable>( t1 T, t2 T ) bool
{
return t1 > t2
}
func1( 2, 1 )
上記はジェネリクス型Tに「comparable」制約を指定しているため、 (Tに関する変数において)大小関係の比較演算子(「>」「>=」または「<」「<=」)を用いた比較が可能となります。 この場合、実際の関数呼び出しにおいて指定する引数の型は、それらの演算子がサポートされた型でなければなりません。
Rarakuではこれは整数、実数、文字列型となります。
逆にそのような演算子がサポートされていない型、例えば構造体などを指定した場合、この制約とは合致しない型となりコンパイルエラーとなります。
その他、算術演算子でも同様の制限がデフォルトで存在します。 すなわちジェネリクスの定義において、算術演算子の使用はデフォルトでは許可されません。 これを許可するには、制約名としてnumericalを指定します。 例えば以下のようになります。
function func1^<T:numerical>( t1 T, t2 T )->
{
return t1 + t2
}
varia ans = func1( 2, 1 )
Rrk_print( \=ans\n )
上記ではジェネリクス型Tにおいて「:numerical」を制約として指定しているため、 Tに関する変数においては、算術演算子(この例では「+」)を用いた演算が可能となります。 算術演算子の演算結果もまたTとなるため、この例でのreturnが返す型はTとなります。 尚、numerical制約の場合、大小比較演算子も同時に使用が許可されます。
上記では型隠蔽演算子「->」を使用しています。
今回の場合、これは戻り値の型で明示的にTと指定したのと全く同じ効果になっています。
列挙型に関する制約
ジェネリクスの引数Tにおいて、それ単独で列挙型と見て欲しい場合もあります。 このためには制約名としてenumを指定します。 例えば以下のようになります(標準関数Rrk_enum_aryの実装はまさにこのようになっています)。
function My_enum_ary^<T:enum>() T[] {
varia dmy_e T
return Rrk_getEnumAry^<T>( dmy_e )
}
上記ではジェネリクス型Tにおいて「:enum」を制約として指定しています。 この関数My_enum_aryは、列挙型を明示的に指定してインスタンシエートを行う形で使用しますが、 その際のジェネリクス引数Tに列挙型以外の型を与えた場合、この関数の入り口においてエラーを表示させることができます。
「:enum」を制約として指定しなかった場合、関数の入り口を通り越して、
関数の実装の内部でエラーを表示するかもしれません。
その場合、ユーザから見れば非常にわかりにくい(実装をむき出しとしたような)エラーとなってしまいます。
構造体メンバに関する制約
Rarakuのジェネリクスにおいては、構造体を指定することもできます。 ただしデフォルトではそのメンバにアクセスするような「.」演算子をジェネリクス内で使うことは許可されません (「==」や「!=」を用いた単純な比較や、代入処理等に限られます)。 これを許可するには、制約名として「struct { メンバ名の羅列 }」を指定します。 例えば以下のようになります。
function dot^<T:struct{m int}>( a T )
{
varia b = a.m
Rrk_print( \=b\n )
}
struct MyStruct {
n conststr
m int
}
varia my MyStruct = { "", 10 }
dot( my )
上記で関数dotにおけるジェネリクス型Tは「:struct{m int}」といった形で制約指定されています。 つまりこのdot関数呼び出し時に引数に指定する型は必ずint型のメンバmを持つ構造体でなければなりません。 この制約におけるメンバは複数指定することもできます。 そのためにはstruct制約の{ ... } 内における指定で通常のstruct文のようにメンバを複数列挙します(ただし通常のstruct文とは異なり、デフォルトメンバ初期化子は指定できません)。 例えば以下のようになります。
function dot^<T:struct{m int p real}>( a T )
{
varia b = a.m
Rrk_print( \=b\n )
}
struct MyStruct {
n conststr
p real
m int
}
varia my MyStruct = { "", 1.5, 10 }
dot( my )
上記でのstruct制約ではint型のメンバm、real型のメンバpを持つ構造体でなければならないことを指示しています。 またこのとき、このメンバの指定(この例ではmとpの指定)の順序は問いません。
ここまでは構造体のメンバの型が(intやrealなどといったように)定義時に具体的に確定しているケースについて取上げました。 次にこの型が定義時に確定していない場合(ジェネリクス型の場合)を取上げます。 例えば「mという名前のメンバ(その型まではわからない)を持つ構造体」を指定したいといった場合です。 この場合、まずジェネリクス型Sを宣言した上で、例えば以下のようにメンバmの型をSとして指定することになります。
function dot^<S, T:struct{m S}>( a T ) /* メンバmはジェネリクス型S */
{
varia b = a.m
Rrk_print( \=b\n )
}
/* int型のメンバmを持つ構造体でdot関数をインスタンシエート */
{
struct MyStruct {
n conststr
m int
}
varia my MyStruct = { "", 10 }
dot( my ) /* A */
}
/* real型のメンバmを持つ構造体でdot関数をインスタンシエート */
{
struct MyStruct {
n conststr
m real
}
varia my MyStruct = { "", 2.5 }
dot( my ) /* B */
}
上記のdot関数の呼び出し(/* A */)において、「メンバmがint型として定義された構造体my」を引数に与えています。 このことからジェネリクス型Tがこの構造体としてインスタンシエートされ、 さらにこのTを経由してジェネリクス型Sの具体的な型がint型であると型推論されることになります。
上記のdot関数の呼び出し(/* B */)においても同様に、「メンバmがreal型として定義された構造体my」を引数に与えていますから、 結果的にSの具体的な型がreal型であると型推論されることになります。
構造体のメンバがどうなっているかについては一切問わないがとにかく(任意の)構造体であってほしい場合、 「:struct{}」といった制約を記述することもできます。 例えば以下の通りです。
function dot^<T:struct{}>( a T ) /* Tは構造体でありさえすればよい */
{
varia b = a
Rrk_print( \=b\n )
}
struct MyStruct {
n conststr
m int
}
varia my MyStruct = { "", 10 }
dot( my )
関数型に関する制約
ジェネリクス型を関数型変数とみなし、関数呼び出しを行う場合を考えます。 この場合、ジェネリクス型の制約名として「function( 引数の羅列 )戻り値の型}」を指定する必要があります。 例えば以下のようになります。
function func^<F:function( x int, y real ) void>( x int, y real, f F )
{
f( x, y )
}
function print( x int, y real ){
Rrk_print( \=x \, \=y \n )
}
func( 1, 2.5, print )
上記ではジェネリクス型Fにおいてfunction制約が指定されているため、その型Fで宣言された引数fは関数型変数とみなされ、 これを介した関数呼び出しをすることが可能です。 またこの場合に指定可能な引数や戻り値の型もfunction制約によって指定します。 上記の例でのfunction制約では、第1引数はint、第2引数はreal、戻り値型はvoidとなっており、 fの呼び出しにおいてもそれらの型に従わなければなりません。
一方、funcの呼び出しにおいて第3引数に指定されている関数もまたこれと同じ関数型である必要があります。 上記では、print関数がこの関数型に合致します。
上記の例でのfunction制約での引数の型は、int、realで確定した型でしたが、 この部分をジェネリクス型にすることもできます。 以下の例をご覧ください。
function func^<S, T, F:function( x S, y T ) bool>( x S, y T, f F )
{
varia b = f( x, y )
Rrk_print( "func:" \=b\n )
}
function less( x int, y real )->{
Rrk_print( "less:" \=x \, \=y \n )
return x < y
}
function more( x real, y int )->{
Rrk_print( "more:" \=x \, \=y \n )
return x > y
}
func(1, 2.5, less) /* A */
func(2.5, 1, more) /* B */
上記のfuncでは、まずジェネリクス型S, Tの使用を予告指定し、それに続けてジェネリクス型Fをfunction制約を伴って指定しています。 Fのfunction制約ではその関数引数の型としてジェネリクス型S, Tを指定しています。 また同じS, Tをfunc関数の引数の型としても使用しています。
実際のfunc関数の呼び出し(/* A */)においては、第1引数をint型、第2引数をreal型で指定し、 第3引数の関数型はそれに準じた関数lessを指定しています。 このことからジェネリクス型S, T もそれぞれint、realと型推論されます。
同様にfunc関数の呼び出し(/* B */)においては、第1引数をreal型、第2引数をint型で指定し、 第3引数の関数型はそれに準じた関数moreを指定しています。 このことからジェネリクス型S, T もそれぞれreal、intと型推論されます。
最後にfunction制約とstruct制約を組み合わせたやや複雑な例も見ておきます。 以下をご覧ください。
function func^< F:function(this T, i int)void, T:struct{method F} >( s T )
{
s.~method( 1 )
}
{
struct MyStruct {
m int
method = ^fun( varia this MyStruct, i int ){
this.m += i
}
}
varia s MyStruct = { 9 }
func( s )
Rrk_print( "s.m=" s.m \n )
}
上記ではまずジェネリクス型Fをfunction制約で指定していますが、この制約における引数thisの型Tもまたジェネリクス型となっています。 またジェネリクス型Tはstruct制約で指定していますが、この構造体のmethodメンバの型は先ほどのジェネリクス型Fとなっています。 func関数の引数sの型はTです。 一方、func関数の呼び出しでは構造体MyStructを指定しています。 構造体MyStruct内にはmethodメンバがあり、その型はジェネリクス型Fと対応していることに注意してください。 これにより最終的にジェネリクス型TはMyStructとなり、ジェネリクス型FはMyStruct内のmethodメンバの型となります。
配列に関する制約
Rarakuのジェネリクスでは、基本的に「T[ ]」といった表記で配列を表現します。 そのため、列挙型や構造体型や関数型で見たような形式での制約を付ける必要は通常ありません。
Rarakuには^a_numof演算子と呼ばれるものが用意されており、これに配列型を指定することによって
その配列型の種類を識別するための要素数を得ることができます(Rrk_numofでは配列型の変数/定数を指定しましたが、^a_numofでは型名を指定します)。
この演算子に固定長配列型を指定した場合、その固定長配列型で指定された要素数(例えばint[3]ならば3)を返します。 可変長配列型を指定した場合、必ずRrk_NPOS(uint64で表現できる最大の数)を返します。 配列型以外の型を指定した場合、必ず 0(uint) を返します。 例えば以下の通りです。
ユーザが直接この演算子を使うことは通常ありませんが、その型が配列型であるか否か、 また配列型である場合は固定長配列か可変長配列のいずれであるか、 そのようなを判定するためにこの演算子を使用します。
この演算子に固定長配列型を指定した場合、その固定長配列型で指定された要素数(例えばint[3]ならば3)を返します。 可変長配列型を指定した場合、必ずRrk_NPOS(uint64で表現できる最大の数)を返します。 配列型以外の型を指定した場合、必ず 0(uint) を返します。 例えば以下の通りです。
varia numof uint
numof = ^a_numof int[3] /* 3 */
numof = ^a_numof int[] /* Rrk_NPOS */
numof = ^a_numof int /* 0 */
ユーザが直接この演算子を使うことは通常ありませんが、その型が配列型であるか否か、 また配列型である場合は固定長配列か可変長配列のいずれであるか、 そのようなを判定するためにこの演算子を使用します。
極めて特殊な例ですが、演算子「^a_numof」を関数内部で使いたい場合、
特別な制約名としてarrayを指定することがあります。
例えば以下のようになります(標準関数Rrk_numofの実装はまさにこのようになっています)。
上記ではジェネリクス型Tにおいて「:array」を制約として指定しているため、 関数My_numofの引数aryに配列以外の型の変数を与えた場合、関数My_numofの入り口においてエラーを表示します。
ただし「:array」を制約として指定した場合、関数の実装の内部ではこれを「^a_numof」演算子に与えることと それがnullか否かを判定すること以外はほぼ何も許可されません。 例えばその要素にアクセスするような記述や、配列自体を文字列に変換するような記述は許可されません。 そのため、Rarakuのユーザ定義関数を作成する用途でこの制約を使用することは、通常ないと思われます。 一方、ネイティブ関数の配列仮引数においては、それがジェネリクス型であれば「T[ ]」の替わりにこの制約を指定することができます。
function My_numof^<A:array>( const ary A ) uint {
return (^a_numof A) == Rrk_NPOS ? Rrk_arynum(ary) :
ary ? (^a_numof A) : 0->uint
}
上記ではジェネリクス型Tにおいて「:array」を制約として指定しているため、 関数My_numofの引数aryに配列以外の型の変数を与えた場合、関数My_numofの入り口においてエラーを表示します。
ただし「:array」を制約として指定した場合、関数の実装の内部ではこれを「^a_numof」演算子に与えることと それがnullか否かを判定すること以外はほぼ何も許可されません。 例えばその要素にアクセスするような記述や、配列自体を文字列に変換するような記述は許可されません。 そのため、Rarakuのユーザ定義関数を作成する用途でこの制約を使用することは、通常ないと思われます。 一方、ネイティブ関数の配列仮引数においては、それがジェネリクス型であれば「T[ ]」の替わりにこの制約を指定することができます。
モディファイアのジェネリクス
関数における(引数や戻り値に付与する)モディファイアそのものをジェネリクスとすることもできます。 これの必要性を理解するためにまずは本題に入る前に次の例を見てみます。
struct Info {
m int
}
function getElem( const ary Info[], idx uint ) const Info^?
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
return null
}
上記は構造体Infoの配列とそのインデックスを引数として受け取り、idx番目の要素を返すといった(ジェネリクスすら登場しない)単純な例ですが、 戻り値の型に付与されたモディファイアconstはこの場合必須になります。 なぜなら(引数の)ary がconstであるとき、(戻り値の)ary[idx] もまたconstとなるためです。 一方、戻り値の型にモディファイアを何も指定しない場合、それはデフォルトでvariaとみなされますが、 構造体の場合、constからvariaへの代入は禁止されますので戻り値の方に明示的にconstを付けなければならないわけです。
ary を本当にリードオンリーで使うならばこれで十分なわけですが、ary[idx] の値を変更し得るような用途で使いたい場合は 上記のモディファイアをすべてvariaにした別の関数を用意する必要があります。 例えば以下の通りです。
struct Info {
m int
}
/* const用 */
function getElem( const ary Info[], idx uint ) const Info^?
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
return null
}
/* varia用 */
function refElem( varia ary Info[], idx uint ) varia Info^?
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
return null
}
const ca Info[] = [ {} ]
const c = getElem( ca, 0 )
varia va Info[] = [ {} ]
varia v = refElem( va, 0 )
上記では、リードオンリーで使う場合はgetElemを、値を変更し得る用途で使いたい場合はrefElemをそれぞれ用いることになります。 しかしながらこのようにするとこれらの関数名を変えなくてはいけません。 また上記をよく見るとその関数の定義部はまったく同じコードになっています。 この種の関数は他にも色々想定できますし、そのようなものを作成する度にconst版/varia版をわざわざ用意するのも冗長です。
そこで本題になりますが、このような場合にモディファイアのジェネリクスを使います。 例えば以下のようになります。
struct Info {
m int
}
/* const/varia共用 */
function atElem^<M:modifier>( M ary Info[], idx uint ) M Info^?
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
return null
}
const ca Info[] = [ {} ]
const c = atElem( ca, 0 )
varia va Info[] = [ {} ]
varia v = atElem( va, 0 )
上記では引数と戻り値の型において、本来モディファイアが指定されるべき位置にMと指定されています。 この場合、Mの部分にはインスタンシエート時に適切なモディファイアが指定されます。 例えば「atElem( ca, 0 )」では引数にconst型の配列が指定されていますので、Mはconstとしてインスタンシエートされますし、 一方「atElem( va, 0 )」では引数にvaria型の配列が指定されていますので、Mはvariaとしてインスタンシエートされます。 さらに戻り値のモディファイアも双方それぞれにおいてconstとvariaに自動的に設定される形になります。 このとき(constを返す)「atElem( ca, 0 )」の戻り値が「const c」にのみ代入可能であり、「varia v」には代入できないことが重要です。 モディファイアのジェネリクスを使うとこのようなことが狙い通り可能になります。
上記のMには、ジェネリクスの制約としてmodifierと指定されていますが、この指定は必須になります。 この指定がない場合、Mはモディファイアではなく型とみなされますのでコンパイルエラーとなります。
nullkindのジェネリクス
関数の引数や戻り値の型に付記する「^?」の有無そのもの(nullableかnotnullか)をジェネリクスとすることもできます。 まずは以下をご覧下さい。
struct Info {
m int
}
/* const/varia共用 Info[]用 */
function atElem^<M:modifier>( M ary Info[], idx uint ) M Info
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
varia default_val Info
return default_val
}
/* const/varia共用 Info[^?]用 */
function atElem2^<M:modifier>( M ary Info[^?], idx uint ) M Info^?
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
varia default_val Info
return default_val
}
const ca Info[] = [ {} ]
const c1 = atElem( ca, 0 )
varia va Info[] = [ {} ]
varia v1 = atElem( va, 0 )
const caq Info[^?] = [ null ]
const c2 = atElem2( caq, 0 )
varia vaq Info[^?] = [ null ]
varia v2 = atElem2( vaq, 0 )
前項で扱った例と非常によく似ていますが、今回はInfoの要素がnullableである場合、 つまり Info[] の他にも Info[^?] についても考えます。 前項で扱ったatElemは Info[] 専用ですので、Info[^?] 型の変数(caq、vaq)を引数に指定することはできません。 そのため上記のようにInfo[^?]用の関数atElem2を別途用意しています。
しかしながら(モディファイアの時の状況とよく似ていますが)このようにすると atElem と atElem2 といったように 関数名を別々にしなくてはならず、またそれにも関わらずそれらの関数の定義部はまったく同じコードになり冗長です。
そこで、このような場合にnullkindのジェネリクスを使います。 例えば以下のようになります。
struct Info {
m int
}
/* const/varia共用、Info[]/Info[^?]共用 */
function atElem^<M:modifier, Q:nullkind>( M ary Info[Q], idx uint ) M Info Q
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
varia default_val Info Q
return default_val
}
const ca Info[] = [ {} ]
const c1 = atElem( ca, 0 )
varia va Info[] = [ {} ]
varia v1 = atElem( va, 0 )
const caq Info[^?] = [ null ]
const c2 = atElem( caq, 0 )
varia vaq Info[^?] = [ null ]
varia v2= atElem( vaq, 0 )
上記ではジェネリクス「Q」の制約として新しくnullkind制約と呼ばれるものを指定しています。 さらに本来「^?」を指定するか省略するかの位置に、その「Q」が記述されていることに注意してください。 例えば上記では「Info[^?」」と書く替わりに「Info[Q]」、「Info^?」と書く替わりに「Info Q」と記述されています。
この場合、Qの部分にはインスタンシエート時に適切に「^?」が指定されるか、さもなくば空として指定されます。 例えば「atElem( ca, 0 )」では引数にInfo[]型の配列が指定されていますので、Qは空としてインスタンシエートされますし、 一方「atElem( caq, 0 )」では引数にInfo[^?]型の配列が指定されていますので、Qは「^?」としてインスタンシエートされます。 nullkindのジェネリクスを使うとこのようにInfo[]とInfo[^?]のどちらの引数も受け付けるような関数を宣言することができます。
globalキーワードが指定された関数のジェネリクス
Rarakuではglobalキーワードが指定された関数においてジェネリクスを定義することはできません。 例えば以下のような記述はコンパイルエラーとなります。
global gfunc^<T>( ary T[], e T ) void { /* コンパイルエラー */
Rrk_print( "hello.\n" )
}
上記は globalキーワードを指定した関数であり、かつジェネリクスを使っており、かつ定義部まで存在するため、 これはコンパイルエラーとなります。 上記のような記述が許可されないということは、例えばジェネリクスの定義をrrksファイル内に(ファイルレベルでの)実装隠蔽することは 基本的にはできないことを意味します(このあたりの仕様はC++のテンプレート等と同様です)。 つまり多くの場合、(他のファイルからでも再利用可能な)ジェネリクスはrrkhファイルに記述する形になると考えられます。
ところで、globalキーワードを指定した関数かつジェネリクスを使っている場合であっても、 それが単なる関数宣言であれば、それは特例として許可されます。 これはモディファイアやnullkindのジェネリクスを使う場合に意味を持つことがあります。 今 util.rrkh、util.rrks、main.rrks の3つのファイルがあるとし、util.rrkhで宣言された関数をmain.rrksで使用するような状況を考えます。 例えば以下をご覧ください。
/* util.rrkh */
struct Info {
m int
}
/* globalの関数(ただし宣言のみ) */
global atElem^<M:modifier>( M ary Info[], idx uint ) M Info
/* util.rrks */
utilize
import util
/* globalの関数の定義(ただしジェネリクスを使用していない) */
global atElem( const ary Info[], idx uint ) const Info
{
if idx < Rrk_numof(ary) {
return ary[ idx ]
}
varia default_val Info
return default_val
}
/* main.rrks */
import util
const ca Info[] = [ {} ]
const c = atElem( ca, 0 )
varia va Info[] = [ {} ]
varia v = atElem( va, 0 )
util.rrkhでは構造体と関数の宣言が書かれており、main.rrksからはそれをimportして使っています。 ここまではいいでしょう。 この例の奇妙なところはutil.rrksでのatElemの定義です。 util.rrkhではatElemはモディファイアのジェネリクスを使って宣言されているにも関わらず、util.rrksでのatElemでは (モディファイアのジェネリクスではなく)constを直接指定した通常の関数とし記述されています。
Rarakuではこのような場合、atElemを使用する位置においては(モディファイアのジェネリクスの宣言に応じて)インスタンシエートされますが、 リンカが最終的にこれの実体をリンクする際、実体として参照する関数名は(余計なマングリング等が入らない)atElemという名前になり、 つまりutil.rrksで定義された通常の関数atElemそのものを実体としてリンクする形となります。 この挙動を利用して、モディファイアのジェネリクスを使った関数の定義をutil.rrks内に閉じ込め、(ファイルレベルでの)実装隠蔽することができます。
このatElemをmain.rrksから使用する分にはこれで何の問題もありません。
ただしutil.rrks内でatElemを実装する観点からは、(util.rrks内で記述している)atElemの実体は、
あくまで引数/戻り値の双方がconstになっているものであることは少し心に留めておく必要があります。
つまりutil.rrkh内でのatElemの宣言は、その実装をリンカの機構によってややアクロバティックにutil.rrks内から拝借している形になります。
また、上記はジェネリクスとしてモディファイアのみが使われた例ですが、以下のように型を含むような通常のジェネリクスの場合 この方法はうまくいきません。
上記の場合、util.rrkhでの宣言とutil.rrks内の定義の型が不一致とみなされエラーとなります。 型を含むようなジェネリクスの場合、多くの場合やはりその実装はrrkhファイル内に直接書く必要があります。
またglobal関数のプロトタイプ側(上記の例ではutil.rrkh内での宣言側)の関数の仮引数がモディファイアのジェネリクスであった場合、 それに対応する実装側(上記の例ではutil.rrks内で記述している側)の関数の仮引数のモディファイアは、 必ずconstである必要があります。 上記の例では、ヘッダ側のatElemの仮引数aryのモディファイアがジェネリクスであるならば、 それに対応する実装側の仮引数のaryでは、そのモディファイアはconstである必要があるということです (constでない場合はコンパイルエラーとなります)。
また、上記はジェネリクスとしてモディファイアのみが使われた例ですが、以下のように型を含むような通常のジェネリクスの場合 この方法はうまくいきません。
/* util.rrkh */
/* globalの関数(型Tを含むようなジェネリクス) */
global atElem^<M:modifier, T>( M ary T[], idx uint ) M T
上記の場合、util.rrkhでの宣言とutil.rrks内の定義の型が不一致とみなされエラーとなります。 型を含むようなジェネリクスの場合、多くの場合やはりその実装はrrkhファイル内に直接書く必要があります。
またglobal関数のプロトタイプ側(上記の例ではutil.rrkh内での宣言側)の関数の仮引数がモディファイアのジェネリクスであった場合、 それに対応する実装側(上記の例ではutil.rrks内で記述している側)の関数の仮引数のモディファイアは、 必ずconstである必要があります。 上記の例では、ヘッダ側のatElemの仮引数aryのモディファイアがジェネリクスであるならば、 それに対応する実装側の仮引数のaryでは、そのモディファイアはconstである必要があるということです (constでない場合はコンパイルエラーとなります)。
実装側の戻り値の型におけるモディファイアは何でも構いません。
実装側の対応する仮引数のモディファイアがconstでなければならない理由は、 もしも仮にそれをvariaなどで指定可能にすると、実装側の関数内で集成体の要素を変更するような処理が書けてしまうからです。 この状態で、さらにconst型の集成体を実引数としてこの関数を呼び出した場合、 その集成体のconstモディファイアが規定する「集成体不変」としてのプロテクトが破綻してしまいます。
実装側の対応する仮引数のモディファイアがconstでなければならない理由は、 もしも仮にそれをvariaなどで指定可能にすると、実装側の関数内で集成体の要素を変更するような処理が書けてしまうからです。 この状態で、さらにconst型の集成体を実引数としてこの関数を呼び出した場合、 その集成体のconstモディファイアが規定する「集成体不変」としてのプロテクトが破綻してしまいます。
auto文
ここでこのセクションで最初に扱ったswap関数の完全版について考えましょう。 まず最初に扱ったswap関数を以下に再掲します。
function swap^<T>( refer x T, refer y T )
{
const tmp T = x
x = y
y = tmp
}
このセクションの最初の方でも述べましたが、 上記の仮引数の「refer」は「refer immut immut」の省略形であり、非集成体ではこれで問題ありませんが、 一方で集成体を指定した場合はモディファイアがimmut immutな集成体しか受容できません。
そこでモディファイアのジェネリクスを利用して、この「refer」の後ろに続くダブルモディファイアをジェネリクス化することを考えます。 例えば以下のようになるでしょう(ただし以下はまだ問題があります)。
function swap^<T, M:modifier, N:modifier>( refer M N x T, refer M N y T )
{
const tmp T = x
x = y
y = tmp
}
上記では、確かに入り口となる仮引数の部分ではすべての集成体を受容できるようになったのですが、 一時変数tmpの宣言におけるモディファイアがconstで固定されているところにまだ問題があります。
このtmpは最後に再代入「y = tmp」の右辺として利用されます。 このとき、ダブルモディファイア「M N」が例えば「varia tight」とインスタンシエートされていた場合、 (tmpはconst constですので)varia tightからconst constへの再代入となってしまい、 ダブルモディファイアのアサイナビリティに違反しますので、これは許可されません。
従ってtmpの宣言におけるダブルモディファイアは「M N」にちょうど一致させる必要があります。 これを実現するために「const tmp T = x」と書く替わりに「M N tmp T = x」と書けばよさそうに見えますが、 Rarakuではパーザ処理系の都合により、文頭にいきなりモディファイアのジェネリクスを書くことはできません。 このような場合替わりに「auto文」を使います。
auto文は、refer文の書式とよく似ており、「auto (ダブルモディファイア)変数名 (型) = 参照される変数」という書式で指定します。 refer文と同じく必ず右辺を明示的に指定しなければなりません(省略は不可です)。
auto文は最終的には「auto」キーワードを取り除いた文と同じ意味になります。 例えば「auto varia const tmp = x」と記述した場合、これは「varia const tmp = x」と書いたのと全く同じです。
auto文が通常の宣言文と異なるのは、そのダブルモディファイアの部分にモディファイアのジェネリクスを指定できることです。 つまり以下のような記述が可能となります。
function My_swap^<T, M:modifier, N:modifier>( refer M N x T, refer M N y T )
{
auto M N tmp = x
x = y
y = tmp
}
上記により、tmpのモディファイアを「M N」に一致させることができ、 最後の再代入「y = tmp」も可能となります。
ちなみにrefer文においても、auto文と同様にダブルモディファイアの部分にモディファイアのジェネリクスを指定することができます。
auto文においてモディファイアを一つも付けなかった場合、refer文と同じくモディファイア推論が行われ、 左辺のモディファイアは右辺のモディファイアと一致します。 そのため(上記のように、auto文にモディファイアのジェネリクスを指定する方法でも特に問題ありませんが)、 上記の替わりに以下のように書くこともできます(標準関数Rrk_swapの実装はまさにこのようになっています)。
function My_swap^<T, M:modifier, N:modifier>( refer M N x T, refer M N y T )
{
auto tmp = x
x = y
y = tmp
}
natvfuncキーワードが指定された関数のジェネリクス
以下のようにnatvfuncキーワードが指定された関数(ネイティブ関数)の宣言においてジェネリクス型を指定することも可能です。
natvfunc nfunc^<T>( ary T[], e T ) void
ネイティブ関数の場合、その実体の定義はRaraku言語では行われません(通常C言語で行われます)から、 必然的にジェネリクスは宣言内でのみ使用される形になります。 Rarakuの標準関数内でもこのような宣言が行われている場合があります。
インスタンシエート済み関数型変数
Rarakuではジェネリクス関数そのものを示す関数型を宣言することはできません。 例えば以下のようなfunctype文はコンパイルエラーとなります。
functype func^<T>( t T ) T /* コンパイルエラー */
ジェネリクス関数そのものを示す関数型の変数を宣言することもできません。 例えば以下のように型指定を省略した宣言をしたとしてもこれは許可されずコンパイルエラーとなります。
function func^<T>( t T )
{
Rrk_print( \=t\n )
}
varia gf = func /* コンパイルエラー */
これは関数型の宣言の情報だけからでは、インスタンシエート不可能であることによります。
一方、ジェネリクス関数を何らかの型で明示的にインスタンシエートした後の関数であれば、 それを示す関数型の変数に代入することが可能です。 例えば以下の通りです。
functype FT_int( t int ) void
function func^<T>( t T )
{
Rrk_print( \=t\n )
}
varia f FT_int = func^<int> /* OK : 明示的なインスタンシエートをしたもので初期化 */
f( 3 )
上記の「func^<int>」のような表記をRarakuではインスタンシエート済み関数型変数と呼びます。 直後に括弧がないため、この時点では関数呼び出しではないことに注意しましょう。
上記では変数fの型をわかりやすくするためfunctype文でその型を明示して書きましたが、 通常はこのfunctype文を省略して以下のように記述することができますし、その方が簡単でしょう。
function func^<T>( t T )
{
Rrk_print( \=t\n )
}
varia f = func^<int> /* OK : 明示的なインスタンシエートをしたもので初期化 */
f( 3 )
このとき変数fの型は型推論により「int型の引数を一つ取り、戻り値がvoidであるような関数型」となります。
複数の型引数があるジェネリクス関数でインスタンシエート済み関数型変数を記述する場合、 以下のように全ての型引数を明示的に指定しなければなりません。 例えば以下の通りです。
function func^<S,T>( s S, t T )
{
Rrk_print( \=s \, \=t \n )
}
varia f = func^<int, real> /* OK : 明示的なインスタンシエートをしたもので初期化 */
f( 3, 2.5 )
上記の「func^<int, real>」の部分を例えば「func^<int>」などとすることはできません。
インスタンシエート済み関数型変数の記述では実引数の指定が存在しない以上、
全ての型引数を明示的に指定しなければ、どういう型でインスタンシエートすればよいかが完全にはわかりません。
そのため、指定に欠けがある場合はコンパイルエラーとなります。
構造体や配列の初期化子を関数引数に直接指定した場合
以下のように構造体の初期化子を関数引数に直接指定した場合を考えます(ただしこれはコンパイルエラーとなります)。
function func^<T, M:modifier>( M s T ) M T {
return s
}
func( { 10, 20 } ) /* Compile error */
上記では少なくとも二つの整数型らしきメンバーをとる構造体が指定されたことはなんとなくわかりますが、 具体的な構造体型の情報がどこにもないため、インスタンシエートできずコンパイルエラーとなります。
次に上記の例において、Tに具体的な構造体型を明示的に指定してみます(ただしMには何も指定しません)。 例えば以下の通りです。
struct Info{
i=0
j=0
}
function func^<T, M:modifier>( M s T ) M T {
return s
}
func^<Info>( { 10, 20 } ) /* Compile error */
一見するとこれでよさそうに見えますが、残念ながらこれもうまくいきません。 現在のRarakuでは構造体の初期化子を直接指定した場合、そのモディファイアは不定となっています。 そのためモディファイアMが関数引数からは自動的には決定できず、やはりコンパイルエラーとなります。
最後に上記の例において、Mにも具体的なモディファイアを明示的に指定してみます。 例えば以下の通りです。
struct Info{
i=0
j=0
}
function func^<T, M:modifier>( M s T ) M T {
return s
}
func^<Info, const>( { 10, 20 } ) /* OK */
上記でようやくすべての情報が揃うためインスタンシエートが可能となり、コンパイルも成功することになります。
もしもRarakuの初期化子のモディファイアが不定ではなくvariaであったなら(そういう仕様であったなら)、
このような場合にモディファイアを明示的に指定することなくコンパイルを成功させることができた
(しかもコンパイラをそのような仕様にすることはそう難しくはない)のですが、
とりあえず現在のRarakuではそのようにはしていません(プログラマの側で明示的な指定が必要ということにしてあります)。
以下のように配列の初期化子を関数引数に直接指定した場合も全く同様です。
function func^<T, M:modifier>( M s T ) M T {
return s
}
func^<int[], varia>( [ 10, 20, 30, 40, ] ) /* OK */
配列の場合、初期化子からなんとなくint[]型の配列であるとわかりそうではありますが、 この場合も具体的な型(今回の場合int[])を明示的に指定しなければなりません。 またモディファイアが要求されている場合はそれも明示的に指定しなければなりません。
その他特殊な状況下におけるジェネリクス
現状のRarakuではジェネリクス関数内に無名関数/ローカル関数やlambda式/lambda関数の定義を書くことはできません。 例えば以下はコンパイルエラーとなります。
function generics_func^<T>( x T ) /* ジェネリクス関数 */
{
function local_func( a int ){ /* コンパイルエラー : ジェネリクス関数内にローカル関数を定義しようとしている */
Rrk_print( \=a\n )
}
lambda lambda_func( a int ){ /* コンパイルエラー : ジェネリクス関数内にlambda関数を定義しようとしている */
Rrk_print( \=a\n )
}
}
これがコンパイルエラーになる理由は主にRaraku処理系の実装上の都合(ジェネリクスに関する意味解析とその妥当性の検証が、現時点では膨大になるため)です。 替わりの手段として、例えば以下のようにグローバルスコープで囲って記述する方法があります。
;{ /* グローバルブロックで囲う */
function local_func^<T>( a T ){
Rrk_print( Rrk_getRtType(a) \n )
}
static generics_func^<T>( x T ) /* ジェネリクス関数 */
{
/* local_funcを使う処理 */
local_func( x )
}
}
generics_func( 3.5 )
上記のようにすることでlocal_funcをグローバルブロックの外側からはアクセス不能とできます。 一方、generics_funcについてはstatic指定しているため、グローバルブロックの外側からでも呼び出せるわけです。 通常は上記でも実用上十分に思えます。 ただしlambda_funcではこの方法は使えません(これをgenerics_funcの外側に追い出した時点で通常の関数と実質同じになるからです)。 グローバルブロックの開始は「{」を使っても構いませんが、「;{」とすることで直前に関数宣言があるような場合にそれと確実に分離できます。
以下のようにジェネリクス関数をdefer文で指定することも可能です。
function func^<T>( x T )
{
}
defer func(0)
ジェネリクス型によるキャストを行うことも可能です。 ただしこの場合インスタンシエート時に展開される実際の型がキャスト可能な組み合わせとなっていなければなりません。 例えば以下のようになります。
function func^<T, S>( x T, varia y S )
{
y = x->S
}
func(1, 2.5)
上記のfunc呼び出し時では、第1引数で指定された型がint、第2引数で指定された型がrealとなっており、 即ちTがint、Sがrealとしてインスタンシエートされます。 「x->S」ではTからSへのキャストを抽象的に定義しておりますが、これはインスタンシエート時にintからreal型へのキャストへと展開されます。 intからrealへのキャストはRarakuでは許可されるため、これはOKとなります。
逆にこのキャストが許可されない組み合わせであった場合、これはコンパイルエラーとなります。
このコンパイルエラーはジェネリクス関数の入口ではなく内部で発生するため、若干分かりにくいものになる可能性があります。
ジェネリクス関数をリカーシブコールすることも可能です。 例えば以下の通りです。
function func^<T:numerical>( t T ) T
{
if t > 1 {
return t * func( t-1 )
}
return 1->T
}
varia ans = func( 4 )
Rrk_print( \=ans\n )
上記ではジェネリクス関数func内で自分自身を呼び出しています。 このときインスタンシエートが無限に再帰的に行われることは勿論なく、ジェネリクス型Tがintとなった実体が一つだけ作成されます。
一つのジェネリクスについて引数の型と戻り値の型が全く同じ場合、それを重複してインスタンシエートすることは一般に意味がなく、
Rarakuでもそれは(内部処理としてやむを得ない場合を除き)基本的には行われません。
目次に戻る
マクロ
マクロの基本的な記法
Rarakuにおけるマクロとは、コード中の特殊な識別子の部分を定義に従って別のトークン列に置き換えるためのものであり、 この展開はコンパイル時に静的に行われます。 似たような記述を何度も大量に記述しなければならず、 しかも今まで扱ってきたもの(関数、lambda式、ジェネリクス等)では対応するのが難しいか、 あるいはそちらを使うとかえって煩雑になってしまうような状況に対応するための最後の手段として使うことができます。
C言語をご存知の方は、Rarakuにおけるマクロはあれとほぼ同じようなものと考えて構いません。
実際、その記法もその展開のされ方もほぼ同じです。
ただしRarakuでは、C言語ではマクロでしか対応できなかったような多くの処理が
lambda式、ジェネリクス等で記述可能ですので、
マクロを使う機会はC言語の場合よりずっと少ないと思われます。
またC言語にあったマクロの問題点がいくつか解消されています。
Rarakuでマクロを定義するにはいくつかの方法があるのですが、 まずは最も単純な#define文から見て行きましょう。 通常この書式は「#define」「名前」「->」「定義値」となります。 例えば以下の通りです。
varia count=0
#define M_DBG->Rrk_print( count "\n" )
count=2
^!M_DBG
count=4
^!M_DBG
count=6
^!M_DBG
上記では、まず#define文によりM_DBGという名前のマクロを定義しています。 #define文は#if文などと同じく必ず行頭から始めるか、あるいは半角スペースかタブのみでインデントされた位置から始めなければなりません。
さらに「->」がいわゆる代入文の「=」のような働きをし、その右側に定義値を書きます。 この場合「Rrk_print」「(」「count」「"\n"」「)」という五つのトークンがM_DBGの定義値になります。 このようにマクロは通常の変数とは異なり、トークン列そのものを定義値として保持します。
C言語をご存知の方は、Rarakuでは「->」が必須であることにご注意ください。
Rarakuでこれが必要な理由は後で詳しく述べますが、C言語におけるマクロにあるいくつかの問題を解消するためです。
このM_DBGを使うにはマクロ展開演算子「^!」を使います。 上記では、「^!M_DBG」と記述されている部分がすべてM_DBGの定義値に置き換えられます。 つまり、上記は次のように記述したのと全く同じです。
varia count=0
count=2
Rrk_print( count "\n" )
count=4
Rrk_print( count "\n" )
count=6
Rrk_print( count "\n" )
この例ではまだマクロの必要性が全く実感できないと思います。 説明の都合上、しばらくはこのような例が続くのでまずはご了承ください。
上記は、確かに全体の記述量は少し減ったように見えますが、同様のことはlambda式を用いてもできます。
例えば以下の通りです。
あるいはcountを引数に取って関数を用いてもよいでしょう。 実際、これらを使う方がマクロよりコードは堅牢です。 今回は記法の説明をなるべくシンプルにするため、このような例を用いましたが、 一般論として関数やlambda式で普通に対応可能ならば、マクロを用いるべきではありません。 Rarakuのマクロは、その定義値がRarakuのトークンであるということまでは把握していますが、 Rarakuの構文や文法までは把握していません。 そのため、定義値であるトークン列にRarakuの文法的に誤りがあった場合でも、 マクロ展開演算子によってマクロが展開された後にコンパイルエラーとなります。 これは換言すれば、マクロの定義部分ではなくマクロの呼び出し位置でコンパイルエラーとなるということです。 従って、そのエラーメッセージが関数やlambda式を用いた場合よりわかりにくいものになる可能性があります。
マクロには関数のように引数を伴うものもあります。
その書式は「#define」「名前」「(」「(関数のようにカンマで区切った)引数(parameter)」「)」「->」「定義値」という形式です。
例えば以下の通りです。
varia count=0
lambda dbg(){ Rrk_print( count "\n" ) }
count=2
dbg()
count=4
dbg()
count=6
dbg()
あるいはcountを引数に取って関数を用いてもよいでしょう。 実際、これらを使う方がマクロよりコードは堅牢です。 今回は記法の説明をなるべくシンプルにするため、このような例を用いましたが、 一般論として関数やlambda式で普通に対応可能ならば、マクロを用いるべきではありません。 Rarakuのマクロは、その定義値がRarakuのトークンであるということまでは把握していますが、 Rarakuの構文や文法までは把握していません。 そのため、定義値であるトークン列にRarakuの文法的に誤りがあった場合でも、 マクロ展開演算子によってマクロが展開された後にコンパイルエラーとなります。 これは換言すれば、マクロの定義部分ではなくマクロの呼び出し位置でコンパイルエラーとなるということです。 従って、そのエラーメッセージが関数やlambda式を用いた場合よりわかりにくいものになる可能性があります。
しかし一方で、仮に誤りがあっても、上記くらいシンプルで目視でも誤りがどこにあるか一目瞭然な例ならば、
何を用いてもそれほど大差はない(つまり別にマクロを用いたとしてもさほど問題ではない)という見方もあるでしょう。
このセクションのテーマはある種の信条に陥りやすいものでもあるので、柔軟な姿勢も必要です。
#define M_PI->3.141592
#define M_RAD2DEG(x)->((x) * 5.729578 * 10)
varia deg = ^!M_RAD2DEG( 2*^!M_PI + 1 )^^2
Rrk_print( \=deg\n )
上記では、M_RAD2DEG(x)が引数ありのマクロになり、「x」がその引数(parameter)になります。 M_RAD2DEGの定義値は「((x) * 5.729578 * 10)」と定義されており、M_RAD2DEGの呼び出し時にはこの「x」の部分に相当する引数(argument)を同時に指定します。 それは(先頭に付加する「^!」を除けば)ちょうど普通の関数の呼び出しのような形式で、 上記では「^!M_RAD2DEG( 2*^!M_PI + 1 )」の部分がその呼び出しに相当します。
次に「^!M_RAD2DEG( 2*^!M_PI + 1 )」が(その定義値である)「((x) * 5.729578 * 10)」に従って展開されます。 これは単に「x」を「2*^!M_PI + 1」に置き替えるだけでよいので、 つまり「((2*^!M_PI + 1) * 5.729578 * 10)」と展開されることになります。
さらに今回は、この結果に「^!M_PI」という別のマクロ呼び出しが含まれているため、展開処理はまだ続きます。 M_PIについては定義値が「3.141592」と定義されていますので、 結局この部分は、最終的には「((2*3.141592 + 1) * 5.729578 * 10)」と展開されることになります。 つまり、上記は次のように記述したのと全く同じです。
varia deg = ((2*3.141592 + 1) * 5.729578 * 10)^^2
Rrk_print( \=deg\n )
M_RAD2DEGのように引数を伴うマクロを関数形式マクロ(function-like macro)と呼ぶことがあります。
一方、最初に挙げたM_DBGやM_PIのように引数を伴わないマクロをオブジェクト形式マクロ(object-like macro)と呼ぶことがあります。
これらは色々な呼び方があり、名称にあまり拘る必要もないのですが、
とりあえずこの記事では両者の区別が必要になった場合は便宜上この呼び方をすることにします。
尚、M_RAD2DEG(x)の定義値を「x * 5.729578 * 10」ではなく「((x) * 5.729578 * 10)」と括弧ありで指定したのは、 演算子(多くの場合算術演算子)の優先順位の関係でプログラマの意図した式と別のものに展開されてしまうのを未然に防止するためです。 例えば「(x)」の部分の括弧をもし付けなかった場合、今回の例ですと 「(2*3.141592 + 1 * 5.729578 * 10)」と展開されることになります (多分期待される式は「((2*3.141592 + 1) * 5.729578 * 10)」のはずですがそれとは異なった結果です)。
また「((x) * 5.729578 * 10)」の一番外側の括弧をもし付けなった場合、今回の例ですと 展開の結果、degに代入すべき右辺全体は「(2*3.141592 + 1) * 5.729578 * 10^^2」となります (多分期待される式は「((2*3.141592 + 1) * 5.729578 * 10)^^2」のはずですがそれとは異なった結果です)。
これらの括弧がない場合でも問題ないケースや、むしろ括弧をつけてはならないケースもありますが、 特に算術演算を伴う式の場合は優先順位が複雑であり、そのマクロが実際にどういう配置で展開されるかも定義の段階ではわからないことも多いため、 通常これらの括弧は付けておいた方が無難でしょう。
尚、関数やlambda式を使えばそもそもこのような問題がありません。
この点も関数やlambda式の方がマクロより望ましいとされる理由の一つです。
全ての引数と式全体を囲う括弧を付けたい場合、勿論上記のように自分で付けてもよいですが、 「->」の替わりに「:=」を指定することで、自動的に括弧を挿入できます。 例えば以下の通りです。
#define M_PI->3.141592
#define M_RAD2DEG(x):=x * 5.729578 * 10
varia deg = ^!M_RAD2DEG( 2*^!M_PI + 1 )^^2
Rrk_print( \=deg\n )
上記では、「M_RAD2DEG(x):=x * 5.729578 * 10」と記述しており、マクロ名と定義値の間には「->」ではなく「:=」を指定しています。 また、定義値は括弧で囲っておりません。 この場合、上記の「x * 5.729578 * 10」の部分は、自動的に「((x) * 5.729578 * 10)」のように括弧が補われます。
C言語と異なり、Rarakuでマクロ名と定義値の間に「->」や「:=」といった特殊な記号が必須な理由は、
一つには「->」か「:=」を切り替えることによってこの括弧を補完するモードのOFF/ONを切り替えるためです。
もう一つはC言語のマクロでは、マクロ名と「(」の間にスペースを入れてしまうと 関数形式マクロではなくオブジェクト形式マクロとみなされてしまうという問題があり、 Rarakuではそれを回避したかったためです。 例えば以下のC言語のマクロ定義をご覧下さい。
C言語では、マクロ名と定義値の間にはスペースを入れる仕様になっています。 ところがその仕様のため、上記のM_RAD2DEG直後の「(x)」は、(M_RAD2DEGと「(x)」の間にスペースが一つあるため) 引数部とはみなさず、定義値の一部とみなされます。 つまり上記は「M_RAD2DEG(x)」という関数形式マクロではなく、「M_RAD2DEG」というオブジェクト形式マクロとみなされ、 その定義値も「((x) * 5.729578 * 10)」ではなく、「(x) ((x) * 5.729578 * 10)」とみなされてしまうということです。
では関数名と直後の括弧の間にスペースがあるならコンパイルエラーにすればよいかと言うとそれも問題です。 そのようにすると、今度は定義値を本当に「(x) ((x) * 5.729578 * 10)」とみなして欲しい場合に、それが不可能になってしまいます。
一方、Rarakuでは「->」か「:=」を入れなければコンパイルエラーになりますので、 マクロ名、引数部、定義値が明確に分断されます。 そのため、上記のような問題が発生しないということになります。
もう一つはC言語のマクロでは、マクロ名と「(」の間にスペースを入れてしまうと 関数形式マクロではなくオブジェクト形式マクロとみなされてしまうという問題があり、 Rarakuではそれを回避したかったためです。 例えば以下のC言語のマクロ定義をご覧下さい。
/* C言語 */
#define M_RAD2DEG (x) ((x) * 5.729578 * 10)
C言語では、マクロ名と定義値の間にはスペースを入れる仕様になっています。 ところがその仕様のため、上記のM_RAD2DEG直後の「(x)」は、(M_RAD2DEGと「(x)」の間にスペースが一つあるため) 引数部とはみなさず、定義値の一部とみなされます。 つまり上記は「M_RAD2DEG(x)」という関数形式マクロではなく、「M_RAD2DEG」というオブジェクト形式マクロとみなされ、 その定義値も「((x) * 5.729578 * 10)」ではなく、「(x) ((x) * 5.729578 * 10)」とみなされてしまうということです。
関数名と後ろに続く括弧との間はスペースを入れないように習慣付けているプログラマ(ちなみに私もそうですが)にとっては
これが問題になることはないと思われますが、その習慣がないプログラマにとってはマクロで躓く一つのポイントになると思われます。
では関数名と直後の括弧の間にスペースがあるならコンパイルエラーにすればよいかと言うとそれも問題です。 そのようにすると、今度は定義値を本当に「(x) ((x) * 5.729578 * 10)」とみなして欲しい場合に、それが不可能になってしまいます。
一方、Rarakuでは「->」か「:=」を入れなければコンパイルエラーになりますので、 マクロ名、引数部、定義値が明確に分断されます。 そのため、上記のような問題が発生しないということになります。
#define文で、以下のようにコメントだけを記述した場合、
定義値が空であるようなマクロを定義することもできます。
上記の「/* none */」は短く「/**/」としても全く構いません。 あるいはコメントでありさえすればよいので、以下のようにさらに短く「//」や「#」と書いても構いません。
一方、#define文では「->」や「:=」の直後に改行を入れるとコンパイルエラーとなります。 また「->」や「:=」の直後から改行までの間がすべて半角スペースかタブだけから構成される場合もコンパイルエラーとなります。
上記がコンパイルエラーになるのは、プログラマが誤って複数行に渡るような定義値を#define文で記述しようとするような兆候を なるべく検出したかったためです。 #define文では複数行に渡る定義を行うことはできません。 そのためには次に述べる#def_begin文を使います。
#define M_EMPTY->/* none */
上記の「/* none */」は短く「/**/」としても全く構いません。 あるいはコメントでありさえすればよいので、以下のようにさらに短く「//」や「#」と書いても構いません。
#define M_EMPTY1->//
#define M_EMPTY2->#
一方、#define文では「->」や「:=」の直後に改行を入れるとコンパイルエラーとなります。 また「->」や「:=」の直後から改行までの間がすべて半角スペースかタブだけから構成される場合もコンパイルエラーとなります。
/* #define文においては、->の直後に改行がある場合はコンパイルエラー */
#define M_EMPTY->
上記がコンパイルエラーになるのは、プログラマが誤って複数行に渡るような定義値を#define文で記述しようとするような兆候を なるべく検出したかったためです。 #define文では複数行に渡る定義を行うことはできません。 そのためには次に述べる#def_begin文を使います。
#define文で、「,」を含む引数を指定したい場合、少し工夫が必要となります。
例えば以下をご覧下さい。
上記では、引数を一つとる関数形式マクロNONEに、引数「{0,0}」を指定しています。 ところがこの指定は「{0」と「0}」の二つの引数とみなされます。 そのため、定義側と呼び出し側の引数の数が一致せずコンパイルエラーとなります。
関数形式マクロの場合、たとえ「{」と「}」で囲まれていようとも「,」が来た時点で「引数の区切り」とみなすため、 このようなことが起こります(一方で、通常の関数における引数の指定では、「{0,0}」が一つの引数とみなされます)。 ただし関数形式マクロであっても、「,」が「(」と「)」で囲まれている場合、その「,」は「引数の区切り」とみなされません。 例えば以下のような指定は(この例では意味はありませんが)一つの引数を与えているとみなされコンパイルエラーとはなりません。
しかしこのような「(」と「)」を付けることができない場合もあります。 例えば以下のように構造体の初期化子として「{0,0}」を指定する場合、 これを「({0,0})」のように「(」と「)」で囲って指定することはできません。
上記はマクロの指定としては文法上問題ありませんが、構造体の初期化子の指定としては (余計な「(」と「)」で括ってあるため)文法上問題があるため、コンパイルエラーとなります。
「(」と「)」を使わずに「,」を引数の区切りとみなさないように指定するためには、 例えば以下のように「,」を一旦別のマクロで宣言し、それを介して間接的に指定する方法があります。
しかしこれは「,」の数が増えると、かなり不恰好なことになりそうです。 それならまだ以下のように、「0,0」をまるごと別のマクロで宣言した方がよいかもしれません。
しかし異なる値の指定の度に、TMP1、TMP2、TMP3など別名のマクロを定義するのも面倒ですし、 またこれがヘッダファイルに記述するコードであった場合、そこにあまり余計なマクロを定義したくはありません。 後で述べるマクロのブロックスコープというものを使えば、名前を変える必要がない上、そのマクロの効果範囲を そのスコープだけに限定できます。このスコープは以下のように#block_scope_beginと#block_scope_endで囲います。
あるいは以下のような関数形式マクロINITを定義して使う方法もあります。
上記の「^!INIT(Struct)({0,0})」の部分を展開すると「^fun( s Struct )->{ ..s }({0,0})」となりますが、 これは無名関数の即時呼び出しとなっています(後半の「({0,0}」の部分はINITマクロとは独立しており、 単に後ろにくっついているだけです)。 またこの無名関数に実際に与えている引数は「{0,0}」となっています(通常の関数への引数であるため、 「{0,0}」という指定は一つの引数とみなされます)。 尚、デフォルトで用意されているRrk_asマクロの中身の実装はまさにこのINITと同じになっています。
#define NONE(a)->/* none */
^!NONE( {0,0} ) /* コンパイルエラー */
上記では、引数を一つとる関数形式マクロNONEに、引数「{0,0}」を指定しています。 ところがこの指定は「{0」と「0}」の二つの引数とみなされます。 そのため、定義側と呼び出し側の引数の数が一致せずコンパイルエラーとなります。
関数形式マクロの場合、たとえ「{」と「}」で囲まれていようとも「,」が来た時点で「引数の区切り」とみなすため、 このようなことが起こります(一方で、通常の関数における引数の指定では、「{0,0}」が一つの引数とみなされます)。 ただし関数形式マクロであっても、「,」が「(」と「)」で囲まれている場合、その「,」は「引数の区切り」とみなされません。 例えば以下のような指定は(この例では意味はありませんが)一つの引数を与えているとみなされコンパイルエラーとはなりません。
#define NONE(a)->/* none */
^!NONE( ({0,0}) ) /* OK */
^!NONE( (0,0) ) /* OK */
これはC言語のマクロでも同様です。
C言語のマクロでも単に「{0,0}」と指定した場合は二つの引数とみなされ、
「({0,0})」と指定した場合は一つの引数とみなされます。
しかしこのような「(」と「)」を付けることができない場合もあります。 例えば以下のように構造体の初期化子として「{0,0}」を指定する場合、 これを「({0,0})」のように「(」と「)」で囲って指定することはできません。
struct Struct {
i int
j int
}
#define SELF(a)->a
varia s Struct = ^!SELF( ({0,0}) ) /* コンパイルエラー */
上記はマクロの指定としては文法上問題ありませんが、構造体の初期化子の指定としては (余計な「(」と「)」で括ってあるため)文法上問題があるため、コンパイルエラーとなります。
「(」と「)」を使わずに「,」を引数の区切りとみなさないように指定するためには、 例えば以下のように「,」を一旦別のマクロで宣言し、それを介して間接的に指定する方法があります。
struct Struct {
i int
j int
}
#define SELF(a)->a
#define COMMA->,
varia s Struct = ^!SELF( {0 ^!COMMA 0} ) /* OK */
しかしこれは「,」の数が増えると、かなり不恰好なことになりそうです。 それならまだ以下のように、「0,0」をまるごと別のマクロで宣言した方がよいかもしれません。
struct Struct {
i int
j int
}
#define SELF(a)->a
#define TMP->{0,0}
varia s Struct = ^!SELF( ^!TMP ) /* OK */
C言語でも構造体の初期化子を「(」と「)」で括ることはできません。
またC言語のマクロでも上記と同様に間接的な指定でこの問題を回避できます。
しかし異なる値の指定の度に、TMP1、TMP2、TMP3など別名のマクロを定義するのも面倒ですし、 またこれがヘッダファイルに記述するコードであった場合、そこにあまり余計なマクロを定義したくはありません。 後で述べるマクロのブロックスコープというものを使えば、名前を変える必要がない上、そのマクロの効果範囲を そのスコープだけに限定できます。このスコープは以下のように#block_scope_beginと#block_scope_endで囲います。
struct Struct {
i int
j int
}
#define SELF(a)->a
#block_scope_begin
#define TMP->{0,0}
varia s0 Struct = ^!SELF( ^!TMP ) /* OK */
#block_scope_end /* TMPマクロの効果はここで一旦終わり */
#block_scope_begin
#define TMP->{1,1}
varia s1 Struct = ^!SELF( ^!TMP ) /* OK */
#block_scope_end /* TMPマクロの効果はここで一旦終わり */
あるいは以下のような関数形式マクロINITを定義して使う方法もあります。
#define INIT(type)->^fun( s type )->{ ..s }
struct Struct {
i int
j int
}
#define SELF(a)->a
varia s Struct = ^!SELF( ^!INIT(Struct)({0,0}) ) /* OK */
上記の「^!INIT(Struct)({0,0})」の部分を展開すると「^fun( s Struct )->{ ..s }({0,0})」となりますが、 これは無名関数の即時呼び出しとなっています(後半の「({0,0}」の部分はINITマクロとは独立しており、 単に後ろにくっついているだけです)。 またこの無名関数に実際に与えている引数は「{0,0}」となっています(通常の関数への引数であるため、 「{0,0}」という指定は一つの引数とみなされます)。 尚、デフォルトで用意されているRrk_asマクロの中身の実装はまさにこのINITと同じになっています。
#define文はすべて一行で定義する必要があります。 しかし、定義値が長い場合や複数の文からなる場合、一行ですべてを記述しては誤りを入れやすくなりますし、 出来上がった記述も後で見辛いものとなってしまいます。
Rarakuでは#def_begin文と#def_end文で囲うことで定義値を複数行(マルチラインフォーム)とすることができます。
この場合、最後に#def_end文が現れるまで定義値の行が続きます。 例えば次の通りです。
#def_begin M_SWAP( x, y )->
auto tmp = x
x = y
y = tmp
#def_end
varia i=10, j=20
Rrk_print( \=i \, \=j \n )
^!M_SWAP( i, j )
Rrk_print( \=i \, \=j \n )
上記はM_SWAPという名前の関数形式マクロを定義しており、その定義値は複数行になっています。 #define文と違い、「->」の直後に改行を置いても構いません(というより通常はここに改行を置くことの方が多いでしょう)。 「auto tmp = x」のある行から#def_end文の直前まで(つまり「y = tmp」とある行まで)が定義値の内容になります。 #def_end文は#def_begin文と同じく必ず行頭から始めるか、あるいは半角スペースかタブのみでインデントされた位置から始めなければなりません。
C言語をご存知の方は、Rarakuでは行末の「\」が不要であることにご注意ください。
C言語の#defineではマクロは一行で書く必要があり、 そのため見かけ上複数行に渡るマクロを記述する場合、以下のように行末を「\」でエスケープする必要がありました。
一方、Rarakuのマルチラインフォームのマクロは本当の意味で複数行であるため、その必要がありません。 以下のように定義値内に「//」や「#」による一行コメントを記述することも問題ありません。
ただし最後の#def_endを書き忘れないようにしましょう。
上記のM_SWAPは、指定された引数xとyを入れ替えるというものですが、
通常の関数とは異なり、その引数に任意の型の変数を指定できます。
しかし、このような処理は既に述べたようにジェネリクスを使っても可能です。
例えば以下の通りです。
C言語の#defineではマクロは一行で書く必要があり、 そのため見かけ上複数行に渡るマクロを記述する場合、以下のように行末を「\」でエスケープする必要がありました。
#define M_SWAP( type, x, y ) do{ \
/* This is imprementation of swap.*/ \
type tmp = x; /* save x for x = y. */ \
x = y; \
y = tmp; \
} while(0)
一方、Rarakuのマルチラインフォームのマクロは本当の意味で複数行であるため、その必要がありません。 以下のように定義値内に「//」や「#」による一行コメントを記述することも問題ありません。
#def_begin M_SWAP( x, y )->{
// This is imprementation of swap.
auto tmp = x # save x for x = y.
x = y
y = tmp
}
#def_end
ただし最後の#def_endを書き忘れないようにしましょう。
function My_swap^<T, M:modifier, N:modifier>( refer M N x T, refer M N y T )
{
auto tmp = x
x = y
y = tmp
}
varia i=10, j=20
Rrk_print( \=i \, \=j \n )
My_swap( i, j )
Rrk_print( \=i \, \=j \n )
マクロで記述したM_SWAPとジェネリクスで記述したMy_swapの違いは何でしょうか?
マクロの方が実装は簡単です(ジェネリックスではモディファイア等も考慮しなくてはなりません)。 しかし例えば誤って x と y に全く別の型の変数を与えてしまった場合、どうなるでしょうか? M_SWAPの場合、これに関するコンパイルエラーはおそらく「x = y」と記述した文において発生するはずです。 つまりマクロの実装部分の込み入った箇所で突然発生するといった形になります。 一方、ジェネリクスでは関数呼び出しの時点で「指定された引数の型が両者でマッチしてない」というような、 通常期待されるコンパイルエラーが表示されます。 そのため、ジェネリクスの方がコードは堅牢であり、ジェネリクスが使用可能な状況なら(マクロよりも)それを使うべきと言えます。
マクロの文字列化演算子
ここまでマクロの書式の説明の都合上、別にマクロを使わなくても可能な例(というよりむしろマクロを使わない方がよい例)ばかりを挙げて来ました。 ここからはマクロなしだと不可能な例や、マクロなしだと酷く煩雑になる例を見ていきましょう。
マクロでは指定した引数のトークン列を文字列リテラルに変換する機能があります。 このためにはマクロ専用の文字列化演算子「#`」を使います。
C言語をご存知の方は、これはC言語のマクロにおける「#」演算子と同類のものと考えて構いません。
例えば以下の通りです。
#define M_DEBUG( x )->( #`x "=[" x "]" )
struct Info {
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
varia info Info
Rrk_print( ^!M_DEBUG( info.ary_[0] ) \n )
Rrk_print( ^!M_DEBUG( info.ary_[1] ) \n )
Rrk_print( ^!M_DEBUG( info.ary_[2] ) \n )
上記では M_DEBUG の定義値内に「#`x」という記述が含まれています。 この場合、M_DEBUGで与えた引数xのトークン列が文字列リテラルに変換されます。
「#`」の直後には必ず(その関数形式マクロにおける)引数(parameter)の識別子(上記の例の場合は「x」)
を記述しなければならないことに注意してください。
それ以外のものを記述した場合はコンパイルエラーとなります。
例えば、マクロの呼び出しにおいて「^!M_DEBUG( info.ary_[0] )」と指定した場合、 マクロの定義部における引数xは「info.ary_[0] 」というトークン列に置き換わります。 これをもう少し分解して書くと「info」「.」「ary_」「[」「0」「] 」という六つのトークンとなりますが、 「#`x」はこれを再連結して"info.ary_[ 0 ]"といった文字列リテラルを生成します。 結局この場合、全体としては「"info.ary_[ 0 ]=[" info.ary_[0] "]"」と展開されることになり、 つまりは「info.ary_[0]」の値が何なのか表示してくれるようなコードになるわけです。 このマクロを使えば「info.ary_[0]」を冗長に二回記述する必要がありません。
この例の実行結果は以下のようになります。
info.ary_[ 0 ]=[apple]
info.ary_[ 1 ]=[orange]
info.ary_[ 2 ]=[banana]
これに関しては関数やlambda式、ジェネリクスなど今まで取り扱ったものでは不可能な処理となります。 それらを使った場合、引数として指定したトークン列そのものの情報は最終的に消失してしまうからです。
指定されるトークン列が単純なもの(単独の識別子や「.」演算子でそれらが繋がったもの)であれば
バックスラッシュ演算子「\=」でも同様なことが可能ではあります(しかもその方が短く書けます)。
しかし、上記の例では指定されるトークン列が配列要素のアクセスを含む複雑なものになっています。
このようなケースではバックスラッシュ演算子「\=」では完全な表示とはなりません。
例えば以下をご覧下さい。
上記は「\=」の後に「info.ary_[0] 」というトークン列が続きますが、 「\=」が認識可能なのは識別子や「.」演算子でそれらが繋がったものに限られます。 そのため、この場合「info.ary_」までを認識します。 その結果、全体としては「"info.ary_=" info.ary_[0]」と展開されることになり、 文字列リテラルの部分に関しては「[0]」が欠けてしまいます (後半の式本体については識別子で始まるトークン列でありさえすればいかなる場合も完全に展開されます)。 この例の実行結果は以下のようになります。
その上、(値が空やスペースの場合でも分かりやすくするなどのために)「=」の直後や値の直後に何かクォート系の文字を入れるような柔軟な対応も 「\=」ではできません。
struct Info {
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
varia info Info
Rrk_print( \=info.ary_[0] \n )
Rrk_print( \=info.ary_[1] \n )
Rrk_print( \=info.ary_[2] \n )
上記は「\=」の後に「info.ary_[0] 」というトークン列が続きますが、 「\=」が認識可能なのは識別子や「.」演算子でそれらが繋がったものに限られます。 そのため、この場合「info.ary_」までを認識します。 その結果、全体としては「"info.ary_=" info.ary_[0]」と展開されることになり、 文字列リテラルの部分に関しては「[0]」が欠けてしまいます (後半の式本体については識別子で始まるトークン列でありさえすればいかなる場合も完全に展開されます)。 この例の実行結果は以下のようになります。
info.ary_=apple
info.ary_=orange
info.ary_=banana
その上、(値が空やスペースの場合でも分かりやすくするなどのために)「=」の直後や値の直後に何かクォート系の文字を入れるような柔軟な対応も 「\=」ではできません。
とはいえ、ユーザに提示するようなある程度パブリックな表示ならともかく、
開発時のデバッグのために(開発者だけがさしあたって)変数の値をたかだか2~3個確認する程度なら
「\=」による不完全な表示でも十分かもしれません。
マクロのトークン連結演算子
マクロでは指定した二つのトークンを連結して一つのトークンに変換する機能があります。 このためにはマクロ専用のトークン連結演算子「##」を使います。
C言語をご存知の方は、これはC言語のマクロにおける「##」演算子とほぼ同類のものと考えて構いません。
例えば以下の通りです。
#def_begin M_DEF_FUNC( type, bit )->
function func##bit( x type##bit )->{ return bit * x }
#def_end
^!M_DEF_FUNC( int, 8 )
^!M_DEF_FUNC( int, 16 )
^!M_DEF_FUNC( int, 32 )
^!M_DEF_FUNC( int, 64 )
上記はマクロは呼び出し側では「^!M_DEF_FUNC( int, 8 )」などとなっており、引数typeには「int」、引数bitには「8」が指定してあります。 そのため、マクロ定義部における「type##bit」は「int」と「8」が連結され、「int8」という一つのトークン(キーワード)が生成されます。 「func##bit」も同様に「func8」となります。さらに引数bitはreturn文において普通に整数リテラルとしても働いています。 結果的に「^!M_DEF_FUNC( int, 8 )」全体は「function func8( x int8 )->{ return 8 * x }」と展開されます。
これは文字列リテラルの連結ではなく識別子と整数リテラルを連結して一つの識別子を作っていることに注意してください。
そういう意味では今までの説明で扱ったものには存在しない極めて特殊な処理といえます。
トークン連結演算子「##」によってどんなトークンも連結可能というわけではなく、 そのオペランドに指定可能なトークンは識別子、Rarakuのキーワード、整数リテラルに限られます。 例えば二つの演算子トークン「+」と「=」を連結して一つの演算子トークン「+=」を作るといったようなことはできません。
C言語のマクロではこれが可能なようですが、Rarakuでは禁止しています。
さらに識別子、Rarakuのキーワード、整数リテラルを自由に組み合わせできるわけではなく、以下のような制限があります。
- 「##」の左側が識別子やRarakuのキーワードの場合
- 「##」の左側が10進数整数リテラルの場合
- 「##」の左側が16進数整数リテラル(0xで始まるリテラル)の場合
「##」の右側は識別子、Rarakuのキーワード、整数リテラルのいずれの指定も可能です。 連結結果は識別子またはRarakuのキーワードとなります。
「##」の右側もまた10進数整数リテラルでなければなりません。 連結結果は10進数整数リテラルとなります。
「##」の右側が10進数整数リテラルである場合はそのまま連結され、 連結結果は16進数整数リテラルとなります。 一方、「##」の右側が16進数整数リテラル(0xで始まるリテラル)である場合、 (右側のリテラルにおける)「0x」の部分が除去された形で連結されます。
もう少し実用性が高いと思われる例を見ましょう。 純粋なRarakuの配列ではないがあたかも配列のような機能を提供するモジュールというものはよくあります。 そのようなモジュールは(内部の実装はどうなっているかはわかりませんが)配列のようにいくつかの要素を保持し、 その要素数を返す関数や、インデックスにより各要素にアクセスできるような関数をモジュールの外部へ公開しているものとします。 例えば以下をご覧下さい。
struct StrAry {
/* private */
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
function StrAry_size( s_ary StrAry ) uint {
/* private */
return Rrk_numof(s_ary.ary_)
}
function StrAry_at( s_ary StrAry, idx uint ) conststr {
/* private */
return s_ary.ary_[ idx ]
}
varia s_ary StrAry
上記はStrAryという名前の構造体を定義しています。 またこのStrAryの内部実装は隠蔽されているものとし(この例ではメンバary_が丸見えですが、 今はこれが隠蔽されていると仮定しましょう)、保持する要素の個数を返す関数StrAry_sizeと インデックスにより各要素へアクセスする関数StrAry_atがモジュールの外部へ公開されているものとします。 つまり、我々がこのモジュールに関して出来ることは(その生成を除いては)StrAry_size、StrAry_atの二つの関数の呼び出しのみです (StrAry構造体の中身に直接アクセスするといったことはできないものとします)。
今、ループ文を使ってStrAryの各要素の値をすべて出力したいとしましょう。 例えばfor文とStrAry_sizeとStrAry_atを使えば、以下のように記述することができます。
struct StrAry {
/* private */
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
function StrAry_size( s_ary StrAry ) uint {
/* private */
return Rrk_numof(s_ary.ary_)
}
function StrAry_at( s_ary StrAry, idx uint ) conststr {
/* private */
return s_ary.ary_[ idx ]
}
varia s_ary StrAry
const size = StrAry_size(s_ary)
for idx:=0u; idx<size; ++idx {
const elem = StrAry_at( s_ary, idx )
Rrk_print( \=elem\n )
}
最後の部分に今回記述したかったfor文があります。 この場合「StrAry_at( s_ary, idx )」の部分がRarakuの配列における「ary[idx]」といった記述に相当することになります。 上記は純粋なRarakuの配列と比べると記述は若干煩雑ではありますが、可読性が低いというほどではなく、 これはこれでよいとは思います。
ただしこのようなモジュールには標準のforeach文は使えません。 別に無理にforeach文を使うことを推奨するわけではありませんが、ここではマクロの使用例として、何とかforeach文のように書きたいと考えたとします。
モジュール関数の名前を規則的にしておく必要はありますが、 以下のようなマクロを使うとそのようなことが割と簡単に実現できます。
#def_begin My_for_each( prefix, elem_decl, ary )->
{
const My_for__size__ = prefix##_size( ary )
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
#def_end
#def_begin My_for_end->
}
}
#def_end
この内容の説明に入る前に、一旦これを使った全体の様子を以下に示しておきます。
struct StrAry {
/* private */
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
function StrAry_size( s_ary StrAry ) uint {
/* private */
return Rrk_numof(s_ary.ary_)
}
function StrAry_at( s_ary StrAry, idx uint ) conststr {
/* private */
return s_ary.ary_[ idx ]
}
#def_begin My_for_each( prefix, elem_decl, ary )->
{
const My_for__size__ = prefix##_size( ary )
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
#def_end
#def_begin My_for_end->
}
}
#def_end
/* use */
varia s_ary StrAry
^!My_for_each( StrAry, const e, s_ary ){
Rrk_print( \=e\n )
}^!My_for_end
事前の準備は大変でしたが、実際に使用しているのは最後の^!My_for_eachと^!My_for_endの部分になります。 本物のforeach文と比べるとまだいくつも不満点はありますが、 多少foreach文っぽさは出たと思います。 ではこのマクロの内容の説明に入りましょう。
上記のMy_for_eachにおいて、「prefix##_size」の部分でトークン連結演算子により新しい関数名を作っています。 例えばprefixにStrAryを指定すれば、これは「StrAry_size」という関数の呼び出しになるというわけです。 これにより、「const My_for__size__ = StrAry_size( ary )」という文がこの位置に出来上がります。 この文はaryの要素数を得て、それをMy_for__size__という一時変数(定数)へ代入するものになります。
次にこの情報を基にfor文を記述しますが、その中の「prefix##_at」の部分で再びトークン連結演算子を使い、先ほどとは別の関数名を作っています。 これにより、「elem_decl = StrAry_at( ary, idx )」という文がこの位置に出来上がります。 この文はインデックスによりaryの要素を取得し、elem_declで宣言された一時変数(定数)に代入するものになります。
elem_declは変数(定数)宣言部となりますが、ここにはユーザがマクロの引数で自由にモディファイアと変数(定数)名を指定することができる点にも注意してください(上記の例では「const e」を指定しています)。 つまりこれ以降、このelem_declの部分に指定した変数(定数)名でモジュールの要素にアクセスすることができ、 結果的にforeachとよく似た外見になるわけです。
このマクロはStrAry以外のモジュールにも適用可能ですが、
ただしそのようなモジュールは、関数名が「X_size」と「X_at」という形式の二つの関数を提供している必要があります
(Xの部分にはモジュール名が入ります)。
さらにその二つの関数は以下のような仕様でなければなりません。
- 「X_size」という形式の関数
- 「X_at」という形式の関数
引数は一つだけで、モジュール本体を引数にとる。 戻り値として内部で保持する要素の個数を返す。
引数は二つあり、モジュール本体とuint型のインデックスをこの順番で引数にとる。 戻り値として内部で保持するインデックス番目の要素を返す。 戻り値の型は各モジュールで異なっていてもよい。
ところでこの「^!My_for_each」では最後に「^!My_for_end」を付けなければなりません。 My_for_eachの実装をよくみると「{」が二つ指定してあり、しかもそれが閉じていません。 My_for_endはこれを閉じるためのもので「}」が二つ指定してあります。
最後に「^!My_for_end」を付けるというのはいかにも不恰好ですが、 もしこれを使わずに書くと、それはそれで今度は以下のように記述しなければならなくなります。
^!My_for_each( StrAry, e, s_ary ){
Rrk_print( \=e\n )
} } }
この場合、一つの「{」に対して三つの「}」がソースコードの字面上に現れます。 つまり「{」と「}」の対応のバランスが崩れてしまい、 おそらくテキストエディタの自動インデント機能に悪影響を及ぼすでしょう。 それに対処するための「苦肉の策」で^!My_for_endというマクロを別途設けて指定しています。
このようなマクロを用意せずに上記の問題に対処する方法もなくはありません。 例えばMy_for_each_2を以下のように定義し、ブロックそのものを最後の引数として指定させる方法です。
#def_begin My_for_each_2( prefix, elem_decl, ary, block )->
{
const My_for__size__ = prefix##_size( ary )
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
block
}
}
#def_end
varia s_ary = StrAry_create()
^!My_for_each_2( StrAry, const e, s_ary, {
Rrk_print( \=e\n )
})
長くなるのでStrAryの定義部分は省略してあります。 上記のMy_for_each_2では最後にもう一つ引数blockを追加しており、 このblockには(My_for_each_2で繰り返す)中身となる無名ブロックそのものを指定します。 「^!My_for_each_2」の呼び出しをよく見ると、四番目の引数として「{ Rrk_print( \=e\n )}」という無名ブロック全体が指定してあることがわかります。
My_for_each_2では、これ一つですべてが完結します。 そのため^!My_for_endが必要だったMy_for_eachと比べれば多少マシにはなりましたが、 しかしこの四番目の引数として無名ブロックそのものを指定するという記述は、少々とっつきにくいかもしれません。 まず「s_ary」の直後に「, {」と続く部分ですが、for文やforeach文を書きなれたプログラマからすると 気を抜くとうっかり「 ){」と書き間違えそうです。また最後の「})」における「)」も忘れずに付ける必要があり、 (「}」に加えて)最後に忘れずに何かを付けなければならないのであれば、結局「^!My_for_end」を忘れずに付けなければならないのと 本質はあまり変わっていないような気もします。
この他にもlambda式を使う方法もあります。
これは大変難しいですが以下の通りです。
最初のマクロM_declare_for_eachの中身は、関数の定義そのものになっています。 注意して頂きたいのは、実際にこの関数が定義されるのは#def_begin-#def_end文においてではなく、 「^!M_declare_for_each( StrAry )」の指定によってこのマクロが呼び出されたタイミングであることです。 このとき、StrAry_for_eachという名前の関数がこの位置において定義されます。 さらにこの関数自体、ジェネリクスのmodifier制約、function制約などを使ったかなり複雑なものであり、 しかもその引数としてlambda式を指定するという今まで扱った内容の総決算のような例題となっています。
ただこの方法は今まで学習した内容のよい復習にはなるかもしれませんが、複雑な割にMy_for_each_2と比べて特に優れた点はないように思います。 最終的に出来上がるStrAry_for_eachは関数となるので、このときにs_aryに指定される引数の型がチェックされるという利点はあるのですが、 記法の簡潔さとしては結局My_for_each_2に劣ります。最後を「})」としなければならない点も変わりませんし、 中身がlambda式であるため、直にbreak文やcontinue文を記述することはできません(return文がbreak文のような機能を果たすことになります)。
#def_begin M_declare_for_each( Module, ElemType )->
function Module##_for_each^<M:modifier, F:function( M v ElemType )void>( M vars Module, func F ){
const size = Module##_size( vars )
for idx:=0u; idx<size; ++idx {
func( Module##_at( vars, idx ) )
}
}
#def_end
^!M_declare_for_each( StrAry, conststr )
varia s_ary = StrAry_create()
StrAry_for_each( s_ary, ^lam( e conststr ){
Rrk_print( \=e\n )
})
最初のマクロM_declare_for_eachの中身は、関数の定義そのものになっています。 注意して頂きたいのは、実際にこの関数が定義されるのは#def_begin-#def_end文においてではなく、 「^!M_declare_for_each( StrAry )」の指定によってこのマクロが呼び出されたタイミングであることです。 このとき、StrAry_for_eachという名前の関数がこの位置において定義されます。 さらにこの関数自体、ジェネリクスのmodifier制約、function制約などを使ったかなり複雑なものであり、 しかもその引数としてlambda式を指定するという今まで扱った内容の総決算のような例題となっています。
ただこの方法は今まで学習した内容のよい復習にはなるかもしれませんが、複雑な割にMy_for_each_2と比べて特に優れた点はないように思います。 最終的に出来上がるStrAry_for_eachは関数となるので、このときにs_aryに指定される引数の型がチェックされるという利点はあるのですが、 記法の簡潔さとしては結局My_for_each_2に劣ります。最後を「})」としなければならない点も変わりませんし、 中身がlambda式であるため、直にbreak文やcontinue文を記述することはできません(return文がbreak文のような機能を果たすことになります)。
最後が「})」ではなく「}」とする方法はないでしょうか? 実はRarakuのマクロではブロック引数という特殊な指定方法があり、これを使うとうまく実現できます。 これについては次の項で述べます。
マクロのブロック引数
ブロック引数とは、マクロの「->」(あるいは「:=」)の直前に「{」「識別子」「}」を指定する書式です (この識別子は必ず一つでなければなりません)。 このように指定した場合、そのマクロの呼び出しの直後に無名ブロックが置かれた場合、 その無名ブロックがマクロ引数として指定されたのと同じ効果になります。 例えば以下をご覧下さい。
#def_begin My_for_each_3( prefix, elem_decl, ary ){ block }->{
const My_for__size__ = prefix##_size( ary )
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
block
}
}
#def_end
varia s_ary = StrAry_create()
^!My_for_each_3( StrAry, const e, s_ary ){
Rrk_print( \=e\n )
}
上記のMy_for_each_3では「->」の直前に「{ block }」という指定があります。 また「^!My_for_each_3」の呼び出しにおいて、その直後に無名ブロック「{ Rrk_print( \=e\n ) }」が置かれています。 このとき、ブロック引数blockの値として(直後の無名ブロックの中身である)「Rrk_print( \=e\n )」が指定されたのと同じ効果になり、 結果的にMy_for_each_3の定義内にあるblockがこの「Rrk_print( \=e\n )」で置換されます。
尚、定義にブロック引数を伴うマクロを呼び出したとき、その呼び出しの直後に無名ブロックがなかった場合はコンパイルエラーとなります。
中身の無名ブロックを引数によって与えるという意味では、これまでに登場したMy_for_each_2と同じです。 ただブロック引数の場合、呼び出し時の引数の与え方が括弧の中ではなく、直後にある無名ブロックであるということです。 つまりこれにより括弧の中に無名ブロックを指定する必要がなくなり、これまでの記法上の不満点はほぼ解消されます。 本物のforeachはさらに括弧を省略することもできますし、StrAryの部分も記述不要ですが、そのあたりはまあ妥協できる点でしょう。
さらに手間をかければStrAryの指定を省略することも不可能ではありません。
結局この指定が必要な理由は、実際の関数の呼び出しにおいて「StrAry_」というプリフィックスが必要となるからです。 これを不要とするには、替わりにメソッドのような形式で「StrAry_at」や「StrAry_size」を呼び出すことができればよいわけです。 そのためには「モジュールと情報隠蔽」のセクションで紹介した方法を使い、 StrAry自体を予め別のモジュールにラップしておきます。 例えば以下の通りです。
見ての通り、前準備が猛烈に面倒です。 またここまでやっても、結局得られる利点は「StrAry」の指定が省略できる程度のことですので、 事前にこの形が提供されているのでない限り、よほどの拘りがなければここまでする意味はあまりないでしょう。
上記のMy_for_each_4では、トークン連結演算子は使用していません。 この例ではこれまでの「StrAry_size( ary )」の替わりに「wrapper.size()」という呼び出しが可能であるためです。 同様に「StrAry_at( ary, idx )」の替わりに「wrapper.at( idx )」といった形式でこれを呼び出します。
実際に使用しているのは「^!My_for_each_4( e, s_ary_w )」の部分で、 確かに「StrAry」の指定は不要となります。 ただしこの s_ary_w はStrAry型ではなくStrAryWrapper型となります。
また少し反則気味ではありますが、もっと単純にマクロをもう一つ用意してStrAryを覆い隠してしまう手もあります。 例えば以下をご覧下さい。
上記のTmp_for_eachの定義値は、^!My_for_each_3マクロにStrAryを指定した形になっています。 これにより、Tmp_for_eachを使う場合は引数にStrAryの指定が不要となります。 つまり「^!My_for_each_3( StrAry, const e, s_ary )」の替わりに「^!Tmp_for_each( const e, s_ary )」と書けるというわけです。 ただし、こうしてしまうとTmp_for_eachはもはやStrAry専用となります。 そのためこのマクロに汎用性はありませんが、プライベートな実装部で即席で短く書きたいという場合には使えるかもしれません。
結局この指定が必要な理由は、実際の関数の呼び出しにおいて「StrAry_」というプリフィックスが必要となるからです。 これを不要とするには、替わりにメソッドのような形式で「StrAry_at」や「StrAry_size」を呼び出すことができればよいわけです。 そのためには「モジュールと情報隠蔽」のセクションで紹介した方法を使い、 StrAry自体を予め別のモジュールにラップしておきます。 例えば以下の通りです。
/* original API */
struct StrAry {
/* private */
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
function StrAry_size( s_ary StrAry ) uint {
/* private */
return Rrk_numof(s_ary.ary_)
}
function StrAry_at( s_ary StrAry, idx uint ) conststr {
/* private */
return s_ary.ary_[ idx ]
}
/* wrap as module */
struct StrAryWrapper {
ary StrAry
at = ^fun( idx uint ) conststr { .."" }
size = ^fun() uint { ..0 }
}
function StrAryWrapper_new()->{
varia self StrAryWrapper = {}
/* public method */
self.at = ^lam( idx uint ) conststr {
return StrAry_at( self.ary, idx )
}
/* public method */
self.size = ^lam() uint {
return StrAry_size( self.ary )
}
return self
}
#def_begin My_for_each_4( elem_decl, wrapper ){
block
}->{
const My_for__size__ = wrapper.size()
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = wrapper.at( idx )
block
}
}
#def_end
varia s_ary_w = StrAryWrapper_new()
^!My_for_each_4( const e, s_ary_w ){
Rrk_print( \=e\n )
}
見ての通り、前準備が猛烈に面倒です。 またここまでやっても、結局得られる利点は「StrAry」の指定が省略できる程度のことですので、 事前にこの形が提供されているのでない限り、よほどの拘りがなければここまでする意味はあまりないでしょう。
上記のMy_for_each_4では、トークン連結演算子は使用していません。 この例ではこれまでの「StrAry_size( ary )」の替わりに「wrapper.size()」という呼び出しが可能であるためです。 同様に「StrAry_at( ary, idx )」の替わりに「wrapper.at( idx )」といった形式でこれを呼び出します。
ただしこのようないわゆるメソッド呼び出しのような既述を実現したいなら、トレイト(trait)を使った方がよいでしょう。
実際に使用しているのは「^!My_for_each_4( e, s_ary_w )」の部分で、 確かに「StrAry」の指定は不要となります。 ただしこの s_ary_w はStrAry型ではなくStrAryWrapper型となります。
また少し反則気味ではありますが、もっと単純にマクロをもう一つ用意してStrAryを覆い隠してしまう手もあります。 例えば以下をご覧下さい。
/* original API */
struct StrAry {
/* private */
ary_ conststr[] = [
"apple",
"orange",
"banana",
]
}
function StrAry_size( s_ary StrAry ) uint {
/* private */
return Rrk_numof(s_ary.ary_)
}
function StrAry_at( s_ary StrAry, idx uint ) conststr {
/* private */
return s_ary.ary_[ idx ]
}
#def_begin My_for_each_3( prefix, elem_decl, ary ){ block }->{
const My_for__size__ = prefix##_size( ary )
for idx:=0u; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
block
}
}
#def_end
#def_begin Tmp_for_each( elem_decl, ary ){ block }->{
^!My_for_each_3( StrAry, elem_decl, ary ){
block
}
#def_end
^!Tmp_for_each( const e, s_ary ){
Rrk_print( \=e\n )
}
上記のTmp_for_eachの定義値は、^!My_for_each_3マクロにStrAryを指定した形になっています。 これにより、Tmp_for_eachを使う場合は引数にStrAryの指定が不要となります。 つまり「^!My_for_each_3( StrAry, const e, s_ary )」の替わりに「^!Tmp_for_each( const e, s_ary )」と書けるというわけです。 ただし、こうしてしまうとTmp_for_eachはもはやStrAry専用となります。 そのためこのマクロに汎用性はありませんが、プライベートな実装部で即席で短く書きたいという場合には使えるかもしれません。
マクロのブロックスコープ
Rarakuでは#block_scope_beginと#block_scope_endで囲った範囲をマクロのブロックスコープと呼び、 このブロックスコープの中で定義したマクロは、この中だけで有効になります。 例えば以下をご覧下さい。
#block_scope_begin
#define M_swap( tmp, x, y )->tmp=x; x=y; y=tmp;
#define M_HELLO->"Hello world."
function swapByte2( tight p uint8[2] ){
varia t uint8
^!M_swap( t, p[0], p[1] )
}
function swapByte3( tight p uint8[3] ){
varia t uint8
^!M_swap( t, p[0], p[2] )
}
function swapByte4( tight p uint8[4] ){
varia t uint8
^!M_swap( t, p[0], p[3] )
^!M_swap( t, p[1], p[2] )
}
function swapByte8( tight p uint8[8] ){
varia t uint8
^!M_swap( t, p[0], p[7] )
^!M_swap( t, p[1], p[6] )
^!M_swap( t, p[2], p[5] )
^!M_swap( t, p[3], p[4] )
}
#block_scope_end
/* out of block scope */
上記ではM_swapやM_HELLOが使用可能なのは#block_scope_beginと#block_scope_endで囲まれたブロックスコープのみとなります。 「/* out of block scope */」とある位置ではこのブロックスコープの外ですので、もうM_swapやM_HELLOを呼び出すことはできません。
これはあくまでマクロにのみ作用するブロックスコープであることに注意してください。
上記のswapByte2、swapByte3、swapByte4、swapByte8についてはマクロではなく純粋な関数ですから何の影響もなく、
「/* out of block scope */」とある位置でももちろん呼び出しが可能です。
このブロックスコープとそれに含まれたマクロの関係は、あたかも通常の「{」「}」によるブロックとそれに含まれた変数の関係のように振舞います。 例えば以下をご覧下さい。
/* global M_swap */
#def_begin M_swap( x, y )->{
varia tmp = x
x = y
y = tmp
}
#def_end
#block_scope_begin
/* local M_swap */
#define M_swap( tmp, x, y )->tmp=x; x=y; y=tmp;
function swapByte2( tight p uint8[2] ){
varia t uint8
^!M_swap( t, p[0], p[1] ) /* call local M_swap */
}
function swapByte3( tight p uint8[3] ){
varia t uint8
^!M_swap( t, p[0], p[2] ) /* call local M_swap */
}
function swapByte4( tight p uint8[4] ){
varia t uint8
^!M_swap( t, p[0], p[3] ) /* call local M_swap */
^!M_swap( t, p[1], p[2] ) /* call local M_swap */
}
function swapByte8( tight p uint8[8] ){
varia t uint8
^!M_swap( t, p[0], p[7] ) /* call local M_swap */
^!M_swap( t, p[1], p[6] ) /* call local M_swap */
^!M_swap( t, p[2], p[5] ) /* call local M_swap */
^!M_swap( t, p[3], p[4] ) /* call local M_swap */
}
#block_scope_end
/* out of block scope */
varia i=10
varia j=20
^!M_swap( i, j ) /* call global M_swap */
この例ではブロックスコープの外側に(多分もっと汎用的に使用可能な)グローバルなM_swapの定義が既に存在します。 しかし今、何か理由があってそれとは別の、しかし名前は同じであるローカルなM_swapをブロックスコープ内に定義したとします。 通常であれば二重定義となってコンパイルエラーとなるところですが、 今回はブロックスコープの中であるためこの定義は許可されます。 この時、このブロックスコープ内においては、(最初のM_swapではなく)二番目のM_swapが優先して呼び出されます。 すなわちswapByte2関数内の「^!M_swap( t, p[0], p[1] )」などの部分によって呼び出されるのは二番目のM_swapです。
一方、このブロックスコープを抜けて「/* out of block scope */」とある位置に至った時点で、二番目のM_swapの有効範囲が終了し、 再び最初のM_swapが有効になります。すなわち一番最後の「^!M_swap( i, j )」によって呼び出されるのは最初のM_swapです。
勿論、別途定義するマクロの名前を変えれば(例えばM_private_swapなどにすれば)、外側のM_swapと衝突はしないのですが、
この場合、事前にどんな名前のマクロが定義されているのかをすべて把握しておくか、
明らかに衝突しないであろう名前を付ける必要があります。
そのような名前の衝突を気にしなくてもよくなるのがブロックスコープの利点となります。
ただし同じブロックスコープ内で同じ名前のマクロを二重に定義することはできません。 この点も通常の「{」「}」によるブロックとそれに含まれた変数の関係と同様です。
ただし同じブロックスコープ内で同じ名前のマクロを二重に定義することはできません。 この点も通常の「{」「}」によるブロックとそれに含まれた変数の関係と同様です。
C言語をご存知の方は、これはC言語のマクロにおける「#undef」と似ていると思われたかもしれません。
しかし#undefでは上記と同様のことは実現できません。
#undefではまずグローバルな(上記の例で言うところの一番目の)M_swapを一旦消去せねばならず(さもなければ二重定義エラーとなります)、
またそれを後で復活させることもできないからです。
尚、Rarakuでは#undef文は提供されません。 #block_scope_begin文と#block_scope_end文でブロックスコープを作ってしまえば#undef文は不要と考えられるからです。
尚、Rarakuでは#undef文は提供されません。 #block_scope_begin文と#block_scope_end文でブロックスコープを作ってしまえば#undef文は不要と考えられるからです。
ブロックスコープはネストすることも一応可能です。 例えば以下をご覧下さい。
#define M_A->"A"
^!M_A /* this value is "A" */
#block_scope_begin
#define M_A->"AA"
^!M_A /* this value is "AA" */
#block_scope_begin
#define M_A->"AAA"
^!M_A /* this value is "AAA" */
#block_scope_end
^!M_A /* this value is "AA" */
#block_scope_end
^!M_A /* this value is "A" */
上記の例では一つの#block_scope_beginと#block_scope_endの間に、もう一つの#block_scope_beginと#block_scope_endがあります。 またグローバルなスコープに定義されたM_A(値は"A")と、ブロックスコープ内で定義されたM_A(値は"AA")と、 ブロックスコープ内のブロックスコープ内で定義されたM_A(値は"AAA")の三つが登場し、 それぞれがアクセス可能な範囲は通常の「{」と「}」によるブロックと同様となります。
ただ、このネストされた(マクロの)ブロックスコープは実際には使い所があまりないかもしれません。
二重のネストくらいであれば場合によってはあるかもしれません。
例えばまず全体を#block_scope_beginと#block_scope_endで囲んで他のファイルと共用しているグローバルスコープと確実に分離し、
さらにその中で必要に応じて、モジュールごとにブロックスコープを区分させるといった使い方はありかもしれません。
しかし三重以上のネストが必要になる状況はさすがにすぐには思いつきません。 そのようなネストが必要なほどマクロを大量に定義するのはそもそも健全ではない可能性が高く、 マクロ以外の別の方法が可能ならそれを検討すべきかもしれません。
しかし三重以上のネストが必要になる状況はさすがにすぐには思いつきません。 そのようなネストが必要なほどマクロを大量に定義するのはそもそも健全ではない可能性が高く、 マクロ以外の別の方法が可能ならそれを検討すべきかもしれません。
#extern文
あるマクロXを実装するためにサブとなるマクロAを定義したとしましょう。 このとき X だけを外部へ公開し、Aを公開しないようにしたい場合があります。 Rarakuのマクロでは#block_scope_beginと#block_scope_end文に加え、#extern文を組み合わせることでこのようなことが可能です。 例えば以下をご覧下さい。
#block_scope_begin
#def_begin Private_for_each_ultimate( prefix, elem_decl, ary, stmt, idx, begin_idx ){
block
}->{
const My_for__size__ = prefix##_size( ary )
stmt
for idx=begin_idx; idx<My_for__size__; ++idx {
elem_decl = prefix##_at( ary, idx )
block
}
}
#def_end
#def_begin Private_for_each_ultimate_dynamic( prefix, elem_decl, ary, stmt, idx, begin_idx ){
block
}->{
stmt
for idx=begin_idx; idx<prefix##_size( ary ); ++idx {
elem_decl = prefix##_at( ary, idx )
block
}
}
#def_end
#def_begin My_for_each_rename_idx( prefix, elem_decl, ary, renamed_idx ){
block
}->{
^!Private_for_each_ultimate( prefix, elem_decl, ary,
varia renamed_idx uint, renamed_idx, 0u ){ block }
}
#def_end
#def_begin My_for_each_outer_idx( prefix, elem_decl, ary, outer_idx ){
block
}->{
^!Private_for_each_ultimate( prefix, elem_decl, ary,
/* none */, outer_idx, 0u ){ block }
}
#def_end
#def_begin My_for_each_dynamic( prefix, elem_decl, ary ){
block
}->{
^!Private_for_each_ultimate_dynamic( prefix, elem_decl, ary,
varia idx uint, idx, 0u ){ block }
}
#def_end
/* public macros */
#extern My_for_each_rename_idx
#extern My_for_each_outer_idx
#extern My_for_each_dynamic
#block_scope_begin
/* ^!My_for_each_rename_idx : OK */
/* ^!My_for_each_outer_idx : OK */
/* ^!My_for_each_dynamic : OK */
/* ^!Private_for_each_ultimate : NG */
/* ^!Private_for_each_ultimate_dynamic : NG */
上記では五つのマクロを定義していますが、そのうち公開したいものは My_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamicの三つです。 Private_for_each_ultimateとPrivate_for_each_ultimate_dynamicについてはこのマクロを実装するための補助用マクロであり、 他のユーザには公開しないものとします。
この場合、まずはこの五つのマクロすべてを一つのブロックスコープに閉じ込めなければなりません。 本当はPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicだけを閉じ込めたいのですが、 そうするとMy_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamic からそれらを使うことができません。
次に#extern文でこれらのうち、ブロックスコープの外部へ公開したいものだけを指定します。 今、My_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamic の三つを外部に公開したいので、 上記の例の最後の部分でそれらを#extern文で指定しています。 これにより、My_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamic の三つがあたかも最初からブロックスコープの 外に定義されていたかのような効果になります。つまりこの三つについては外部から呼び出すことが可能になります。 逆に、#extern指定されていないPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicについては呼び出すことはできない形になり、 当初の狙い通りの結果となります。
C言語のマクロについて熟知されている方は次のように考えたかもしれません。
My_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamic の三つの定義値には Private_for_each_ultimateとPrivate_for_each_ultimate_dynamicというトークン列を含みます。 RarakuのマクロがC言語のマクロと同様に展開されるのであれば、#def_beginと#def_end文による定義位置ではなく、 実際にマクロが呼び出されるタイミングで、(依存マクロを含め)すべてが展開される形になるはずです。
つまりPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicも、 定義の段階では展開されずに残り、呼び出し時に展開されることになります。 そうなるとPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicが最終的に外部から呼び出せないようでは この展開処理ができず困るのではないでしょうか?
#extern文の呼び出しの時点で依存するマクロがすべて展開されるのであればこの問題を解決できそうですが、 このようにした場合、マクロの展開規則をそこで捻じ曲げることになります。 よってRarakuではそのようには処理していません。
Rarakuの#extern文で行われていることはマクロの依存関係をリンク構造として形成することです。 喩えるならPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicは透明になって見えなくなっているようなものです。 実際にはまだ内部にその実体があり、外部から「直には」呼び出せませんが、#extern文で指定した文(My_for_each_rename_idx、 My_for_each_outer_idx、My_for_each_dynamic の三つ)からだけはそれらが見え、それらを呼び出すことができる仕組みになっています。
この依存関係が多段階になっている場合はこの関係が芋づる式に繋がることになります。 例えばマクロAはマクロBに依存し、マクロBはマクロCに依存するとします。 ここで「#extern A」と指定した場合、AからはBが見え、BからはCが見えるといった関係が自動的に形成された上で Aが外部に公開されることになります。
My_for_each_rename_idx、My_for_each_outer_idx、My_for_each_dynamic の三つの定義値には Private_for_each_ultimateとPrivate_for_each_ultimate_dynamicというトークン列を含みます。 RarakuのマクロがC言語のマクロと同様に展開されるのであれば、#def_beginと#def_end文による定義位置ではなく、 実際にマクロが呼び出されるタイミングで、(依存マクロを含め)すべてが展開される形になるはずです。
つまりPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicも、 定義の段階では展開されずに残り、呼び出し時に展開されることになります。 そうなるとPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicが最終的に外部から呼び出せないようでは この展開処理ができず困るのではないでしょうか?
#extern文の呼び出しの時点で依存するマクロがすべて展開されるのであればこの問題を解決できそうですが、 このようにした場合、マクロの展開規則をそこで捻じ曲げることになります。 よってRarakuではそのようには処理していません。
Rarakuの#extern文で行われていることはマクロの依存関係をリンク構造として形成することです。 喩えるならPrivate_for_each_ultimateとPrivate_for_each_ultimate_dynamicは透明になって見えなくなっているようなものです。 実際にはまだ内部にその実体があり、外部から「直には」呼び出せませんが、#extern文で指定した文(My_for_each_rename_idx、 My_for_each_outer_idx、My_for_each_dynamic の三つ)からだけはそれらが見え、それらを呼び出すことができる仕組みになっています。
この依存関係が多段階になっている場合はこの関係が芋づる式に繋がることになります。 例えばマクロAはマクロBに依存し、マクロBはマクロCに依存するとします。 ここで「#extern A」と指定した場合、AからはBが見え、BからはCが見えるといった関係が自動的に形成された上で Aが外部に公開されることになります。
あまりないことかもしれませんが、ブロックスコープが何個もネストされている状況で#extern文を使った場合、
#extern文で指定されたマクロは一つ上の階層へ公開されます。
例えば以下をご覧下さい。
上記は二重にネストされた例ですが、その最深部にマクロM_Aが定義されています。 このままではこの最深部のブロックスコープ(上記の(1)の部分)でしかマクロM_Aを呼び出せませんが、 「#extern M_A」文をそこで呼び出すことで、一つ上の階層にM_Aが公開され、上記の(2)の部分からもマクロM_Aを呼び出すことができるようになります。
ただここで止めた場合、一番外側のグローバルなスコープ(上記の(3)の部分)ではマクロM_Aを呼び出せません。 これを可能にするためには、上記(2)の位置で再び「#extern M_A」を呼び出し、さらにもう一つ上の階層にM_Aを公開しなければなりません。 これにより、上記(1)、(2)、(3)のすべてからマクロM_Aを呼び出すことができるようになります。
#block_scope_begin
#block_scope_begin
#define M_A->"AAA"
^!M_A /* (1) this value is "AAA" */
#extern M_A
#block_scope_end
^!M_A /* (2) this value is "AAA" */
#extern M_A
#block_scope_end
^!M_A /* (3) this value is "AAA" */
上記は二重にネストされた例ですが、その最深部にマクロM_Aが定義されています。 このままではこの最深部のブロックスコープ(上記の(1)の部分)でしかマクロM_Aを呼び出せませんが、 「#extern M_A」文をそこで呼び出すことで、一つ上の階層にM_Aが公開され、上記の(2)の部分からもマクロM_Aを呼び出すことができるようになります。
ただここで止めた場合、一番外側のグローバルなスコープ(上記の(3)の部分)ではマクロM_Aを呼び出せません。 これを可能にするためには、上記(2)の位置で再び「#extern M_A」を呼び出し、さらにもう一つ上の階層にM_Aを公開しなければなりません。 これにより、上記(1)、(2)、(3)のすべてからマクロM_Aを呼び出すことができるようになります。
#extern文でマクロを一つ上の階層へ公開しようとしたとき、
既に一つ上の階層で同じ名前のマクロが定義されている場合(あるいはそれに相当する状況の場合)はコンパイルエラーとなります。
例えば以下をご覧下さい。
上記では「#extern M_A」によりM_Aを一つ上の階層へ公開しようとしていますが、 一つ上の階層には既にM_Aが定義されており、コンパイルエラーとなります。
また以下のようなケースも最終的にM_Aが同一の階層に定義されているとみなされ、コンパイルエラーとなります。
上記では、一度目の「#extern M_A」で一つ上の階層にM_Aの定義が発生したと考えることができます。 そのため、二度目の「#extern M_A」では、既に一つ上の階層に同名のマクロが定義されているとみなされ、二重定義のエラーとなります。
コンパイルエラーとなるのはあくまで「同一の階層」にマクロが二重定義される場合だけです。 例えば以下のように定義位置に相当する階層が異なるのであれば構いません。
この場合、一つ上の階層の(1)の位置で参照されるのは#externで指定されたM_Aの方になります。 これは結局ブロックスコープの中で定義されたマクロは外側で定義されたマクロより優先して参照されるという一般規則に従ったものとなります。
#define M_A->"A"
#block_scope_begin
#define M_A->"AAA"
#extern M_A /* Compile error. M_A is already defined in super block */
#block_scope_end
上記では「#extern M_A」によりM_Aを一つ上の階層へ公開しようとしていますが、 一つ上の階層には既にM_Aが定義されており、コンパイルエラーとなります。
また以下のようなケースも最終的にM_Aが同一の階層に定義されているとみなされ、コンパイルエラーとなります。
#block_scope_begin
#define M_A->"AAA"
#extern M_A
#block_scope_end
#block_scope_begin
#define M_A->"AAA"
#extern M_A /* Compile error. M_A is already defined in super block */
#block_scope_end
上記では、一度目の「#extern M_A」で一つ上の階層にM_Aの定義が発生したと考えることができます。 そのため、二度目の「#extern M_A」では、既に一つ上の階層に同名のマクロが定義されているとみなされ、二重定義のエラーとなります。
コンパイルエラーとなるのはあくまで「同一の階層」にマクロが二重定義される場合だけです。 例えば以下のように定義位置に相当する階層が異なるのであれば構いません。
#define M_A->"A"
#block_scope_begin
#block_scope_begin
#define M_A->"AAA"
#extern M_A /* OK */
#block_scope_end
^!M_A /* (1) this value is "AAA" */
#block_scope_end
^!M_A /* (2) this value is "A" */
この場合、一つ上の階層の(1)の位置で参照されるのは#externで指定されたM_Aの方になります。 これは結局ブロックスコープの中で定義されたマクロは外側で定義されたマクロより優先して参照されるという一般規則に従ったものとなります。
目次に戻る
Appendix
Appendix 1 : volatile
Rarakuでは最適化のため、右辺値が整数リテラルであるconst型の定数をコード内で(識別子として)使用すると、 それは自動的に整数リテラルそのものとしてコード内に展開されます。 例えば以下の通りです。
const c = 3 /* rhs is integer literal */
varia v = 5
const a = c + v /* same as 3 + v */
上記の1行目では、右辺値が整数リテラルであるconst型の定数cを宣言しています。 また上記の3行目では、その c を使用していますが、 このとき識別子cは整数リテラル「3」そのものに展開され、 直接「3 + v」と書いたのと同じになります。 計算結果 a の値は結局同じ( 8 )ではありますが、これにより「c + v」と書いたのよりほんの少しだけ高速になります。
なんらかの理由でこの展開処理をして欲しくない場合は volatile キーワードをconstの前に付けます。 例えば以下の通りです。
volatile const c = 3 /* rhs is integer literal */
varia v = 5
const a = c + v /* no optimized */
Acknowledgment
Raraku言語を実装するにあたっては、以下のWebサイトや文献を参考にさせてもらいました。 また極一部ではありますがその内容も紹介させて頂きます。
- The Go Programming Language Specification (https://go.dev/ref/spec)
- Swift LANGUAGE GUIDE (https://docs.swift.org/swift-book/documentation/the-swift-programming-language)
- The Rust Programming Language (https://doc.rust-lang.org/book)
- Programming concepts (C#) (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/methods)
- Nim Manual (Andreas Rumpf, Zahary Karadjov) (https://nim-lang.org/docs/manual.html)
- The Python Language Reference (https://docs.python.org/3.14/reference)
- Garbage Collection for Python (http://arctrix.com/nas/python/gc/)
- MDN Javascript Reference (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference)
- Kotlin Language guide Null safety (https://kotlinlang.org/docs/null-safety.html)
- The Java Virtual Machine Specification Java SE 8 Edition(Tim Lindholm, Frank Yellin, Gilad Bracha, Alex Buckley) (https://docs.oracle.com/javase/specs/jvms/se8/html/)
- Linkers and Loaders (John R. Levine)
- いまどきのプログラム言語の作り方(Randy著)
- プログラミング言語処理系(佐々正孝著)
- Cプログラマのためのアルゴリズムとデータ構造(近藤嘉雪著)
- プログラミング言語を作る(前橋和弥著)
- コンパイラ構成法(原田賢一著)
- スモールコンパイラの製作で学ぶプログラムのしくみ(石田綾著/中田育男監修)
- オブジェクト指向スクリプト言語Ruby(まつもとゆきひろ/石塚圭樹著)
Go言語の公式Webサイトです。 Rarakuは全体的にC言語とGo言語の文法にかなり強い影響を受けています。 例えばif文の条件式の中に宣言を同時に書く記法やfallthrough文、defer文などは(他の言語にも同様のものは存在はしますが)概ねGoを基準としています。
Swift言語の公式Webサイトです。 RarakuではSwift言語の文法を少し参考にさせてもらっています。
Rust言語の公式Webサイトです。 Rarakuでは表面的な文法としてはRustにそこまで影響を受けていませんが、Rustは非常に安全な言語であるため、 その仕様の根底にある考え方には多かれ少なかれ影響を受けているとは思います。
C#言語の公式Webサイトです(MSのサイトはゴチャゴチャしていて正直どこを挙げればよいのかよくわかりませんが一応私が参照したURLを挙げておきます)。 Rarakuでは文法的にはヒアドキュメントの記法や関数の名前付き引数の指定方法などでC#の文法を参考にさせてもらっています。
Nim言語の公式Webサイトです。 Rarakuでは実はNimも少し参考にしています。 特に文字列連結演算子「&」はNimから拝借させていただきました。 またRarakuのビット演算子はC言語のそれと大きく違いますが、この部分もNimの影響を受けています。
Python言語の公式Webサイトです。 Rarakuではlambda、globalキーワードなど(その仕様は異なりますが)意図的にPythonに似せている側面も若干あります。 Rarakuでは言語仕様についてはほとんどPythonを参考にしていませんが、次に述べますGCの仕様についてはCPythonのものを大いに参考にしています。
Neil Schemenauer氏によるWebサイトです。 この方はCPythonの作者でもあります。 このサイトではPythonにおけるGC(Garbage Collection)がどのようなアルゴリズムで実装されているかについて詳しく解説しています。 RarakuのGCはこのアルゴリズム(循環参照にそれなりに対処したリファレンスカウンタ方式)の考え方をほぼそのまま採用しており、 GC開発において最も基本的な情報源となりました。
MDN(Mozillaの開発者向けサイト)によるリファレンスです。 Javascriptの公式Webサイトと言ってよいかどうかはわかりませんが、実質的にはここが最も代表的なリファレンスサイトになると思います。 Rarakuではfunctionキーワードなど(その仕様は異なりますが)意図的にJavascriptに似せている側面も若干あります。
Kotlin言語の公式Webサイトです。 KotlinはJavaと並びAndroidアプリでよく使われる言語です。 特にNull安全性に関してスマートキャストと呼ばれる仕組みが備わっており、Rarakuではそれを一部参考にさせてもらっています。
これはJava仮想マシンの公式な仕様書になるかと思います。 Rarakuでは言語仕様についてはあまりJavaを参考にしていませんが、一方で仮想マシンの仕様については、 Java(VM)はやはり詳しい資料になろうかと思います。 特に.classファイル形式は世界的に広く普及したファイルフォーマットですから、その構成については興味があるところです。
文字通りリンカとローダに関する本です。 これはプログラミング言語作成のための専門書というわけではありませんが、様々なOSにおけるオブジェクトファイル、 実行ファイル、ライブラリファイルのフォーマットについて説明されており、 Rarakuにおけるrrko、rrkxファイルの仕様策定において参考になりました。 また静的リンカと動的リンカの実装、グローバル関数のインポート、エクスポートとそのシンボルに対するリゾルバ、およびリアロケーションの説明もあり、 Rarakuのリンカとローダの実装にあたっては基本的に重要な本でした。
古い本で現在ではもう入手が困難と思いますが、Javaを使い字句解析や構文解析もすべて自前で実装した上で、 JoyToyと呼ばれるプロトタイプベースの言語を作成する本です。 プログラミング言語を初めて作る方向けの入門書として最適かと思います。
プログラミング言語処理系を実装するにあたって、ほぼ全般的な理論が学べる教科書になるかと思います。 世界的に有名な教科書としては、いわゆるドラゴンブック (Compilers Principles, Techniques, and Tools(Alfred V.Aho, Ravi Sethi, and Jeffrey D.Ullman)) がありますが、 例えば構文解析における左再帰の除去や、goto文とラベル文におけるバックパッチング法などに関する説明など 多くに関してこの本の方がわかりやすいかもしれません。 またPL/0と呼ばれる言語に対応した仮想マシンのC言語による実装が付録に掲載されています。
これはプログラミング言語作成のための専門書というわけではありませんが、特に文字列の探索の章が素晴らしく、 字句解析フェーズにおいて重要なコードの宝庫です。 正規表現を解析するための完全なコードも掲載されています。 Rarakuではlexを用いず字句解析も自前で実装しており、その実装の中で同時に正規表現の解析も行っておりますが、 その際に参考にさせてもらったのがこの本になります。
著者は「C言語ポインタ完全制覇」で有名な方ですが、プログラミング言語作成についての本も執筆されています。 この本は主に実装面におけるテクニック、例えば各種データ構造のC言語コード上での実現/管理などについて、詳しく説明されてあります。 またCを用いてDVM(Diksam Virtual Machine)と呼ばれる仮想マシンを作成し、さらにそれをベースとしてDiksamと呼ばれる静的型付き言語を実装しています。
古い本で現在ではもう入手が困難と思いますが、実装面を重きに置いたコンパイラの教科書です。 ちなみに著者は邦訳版ドラゴンブックを翻訳した方でもあります。 この本もまた実装面におけるテクニックの記載に詳しく、C言語ライクな言語を作成するにあたって、 そのユーザ定義型をテーブルによってどのように管理するかなどが説明されてあります。 またCを用いてVSM(Virtual Stack Machine)と呼ばれる仮想マシンを作成し、さらにそれをベースとしてTypeLと呼ばれるほぼC言語を模した静的型付き言語を実装しています。
わかりやすい図解と誤植が多い本です。 例えばディスプレイと呼ばれる概念を使って関数呼び出しを管理する方法や、レベルによって記憶域(グローバル変数やスタック)を整理する考え方が説明されていあります。 この本ではJavaを用いてCell/VMと呼ばれる仮想マシンを作成し、さらにそれをベースとして携帯電話上で動作する言語を実装しています。
RubyはC言語によってRuby自体を拡張する機能があります。 RarakuにもRaraku Extensと呼ばれる同様の機能がありますが、 この本はRubyではそれをどのように実現しているかが説明されてあります。
上記の文献の著者の方々にはこの場を借りてお礼申し上げます。
目次に戻る
This article was written by:
Mr.Moai
@znk project