第2章 言語
この章では、この言語の文法と意味について述べる。
文法構造
識別子
識別子はアルファベット文字もしくは '_' で始まり、任意の数のアルファベット文字か '_' か数("0"から"9")が続いたものである。
Squirrelは大文字小文字を区別するため、大文字と小文字を異なる文字として取り扱う。
例えば、"foo"と"Foo"と"fOo"は3つの異なる識別子として扱われる。
id := [a-zA-Z_]+[a-zA-Z_0-9]*
キーワード
次の単語はこの言語によって予約された語であり、識別子として用いることはできない。
break | case | catch | class | clone | continue |
default | delegate | delete | else | extends | for |
function | if | in | local | null | resume |
return | switch | this | throw | try | typeof |
while | parent | yield | constructor | vargc | vargv |
instanceof | true | false | static |
これらのキーワードの詳細は後に説明する。
演算子
Squirrelは次の演算子を認識する。
! | != | || | && | <= | >= | > | |
+ | += | - | -= | / | /= | * | *= |
% | %= | ++ | -- | <- | = | & | ^ |
| | ~ | >> | << | >>> |
他のトークン
他に使用されるトークンは次の通り。
{ } [ ] . : :: ' ; " @"
リテラル
Squirrelでは整数、浮動小数点数、文字列リテラルを使用できる。
34 | 整数 |
0xFF00A120 | 整数 |
'a' | 整数 |
1.52 | 浮動小数点数 |
1.e2 | 浮動小数点数 |
1.e-2 | 浮動小数点数 |
"I'm a string" | 文字列 |
@"I'm a verbatim string" | 文字列 |
@" I'm a multiline verbatim string " | 文字列 |
IntegerLiteral := [0-9]+ | '0x' [0-9A-Fa-f]+ | ''' [.]+ ''' FloatLiteral := [0-9]+ '.' [0-9]+ FloatLiteral := [0-9]+ '.' 'e'|'E' '+'|'-' [0-9]+ StringLiteral := '"' [.]* '"' VerbatimStringLiteral := '@' '"' [.]* '"'
コメント
コメントは、コンパイラは無視するがプログラマには有用なテキストである。コメントは通常、コードの注釈を埋め込むために用いられる。コンパイラはこれらを空白として取り扱う。
スラッシュ、アスタリスク(/*)に始まり、*/が出現するまでの改行を含む全ての文字。この文法はANSI Cと同じである。
/* これは 複数行コメントである。 これらはコンパイラによって無視される。 */
ふたつのスラッシュ( // )。これは直前にバックスラッシュがない改行まで続く。これは一般に「一行コメント」と呼ばれる。
//これは一行コメントである。これはコンパイラによって無視される。
値とデータ型
Squirrelは動的型言語であるため、型を持つ値へと参照している場合でも、変数は型を持たない。
Squirrelの基本型は整数、浮動小数点数(float)、文字列、null、テーブル、配列、関数、ジェネレータ、クラス、インスタンス、真偽値、スレッド、ユーザデータである。
整数
32ビット(もしくはそれ以上)の符号付き整数を表現する。
local a = 123 // 10進数 local b = 0x0012 // 16進数 local c = 'w' // 文字コード
浮動小数点数(float)
32ビット(もしくはそれ以上)の浮動小数点数を表現する。
local a = 1.0 local b = 0.234
文字列
文字列は不変な文字列シーケンスであり、文字列を変更するには新しい文字列を作成する必要がある。
Squirrelの文字列はCやC++のように振る舞い、二重引用符 " によって区切られ、エスケープシーケンス(\t, \a, \b, \n, \r, \v, \f, \\, \", \', \0, \xhhhh)を含めることができる。
逐語的文字列(verbatim string)のリテラルは @" で始まり、対応する " で終了する。逐語的文字列のリテラルは改行を越えることができる。そのとき、二重引用符の間のホワイトスペースを含めることができる。
local a = "I'm a wonderful string\n" // は文字列の最後に改行を持つ local x = @"I'm a verbatim string\n" // この \n は 通常の文字列での \\n としてコピーされる
逐語的文字列リテラルの「エスケープシーケンスなし」ルールの唯一の例外は、二重引用符である。
逐語的文字列中の二重引用符は2回連続させる必要がある。
local multiline = @" これは複数行の文字列である。 これは全ての改行を ""埋め込む"" ことができる。 "
Null
無効、空、存在しない参照を表現するプリミティブである。null型はただひとつの値を持ち、それはnullと呼ばれる。
local a = null
真偽値型
真偽値型は2つの値だけを持つ。これらのリテラルはtrueとfalseである。
真偽値は条件の検証(条件が真か偽かを表現する)に用いられる。
local a = true;
テーブル
テーブルは、キーと値のペア(スロットと呼ばれる)として実装された連想コンテナである。
local t = {} local test = { a = 10 b = function(a) { return a + 1; } }
配列
配列はオブジェクトの単純なシーケンスであり、そのサイズは動的であり、添字は常に0から始まる。
local a = ["私", "は", "配列"] local b = [null] b[0] = a[2];
関数
関数は一般には他のC風の言語とほとんどのプログラミング言語のものと似ている。
しかし、若干の違いがある(後述)。
クラス
クラスは、キーと値のペアとして実装された連想コンテナである。クラスはクラス式またはクラス命令文を通して作成される。
クラスメンバは、他のクラスオブジェクトのひとつから作成時に継承される。クラス作成後には、クラスのインスタンスが作成されるまで、メンバを追加することができる。
インスタンス
クラスインスタンスはクラスオブジェクトを呼び出すことで作成される。テーブルと同様、インスタンスはキーと値のペアで実装されている。
インスタンスのメンバは動的に追加や削除を行うことができないが、その値を変更することはできる。
ジェネレータ
ジェネレータはyield命令によって中断でき、後で再開ができる関数である(ジェネレータの項を参照せよ)。
ユーザデータ
ユーザデータオブジェクトは、ホストアプリケーションによって定義されているがSquirrelの変数中に蓄えられているメモリの塊(やポインタ)である(ユーザデータとユーザポインタの項を参照せよ)。
スレッド
スレッドは協調スレッド(これはコルーチンとして知られる)の実行を表現するオブジェクトである。
弱参照
弱参照は、他の(数値ではない)オブジェクトを差すが、それへの強参照を所有しないオブジェクトである(弱参照の項を参照せよ)。
実行コンテキスト
実行コンテキストは関数スタックフレームと関数環境オブジェクト(this)の和集合である。
スタックフレームはスタックの一部であり、その本体で宣言されたローカル変数が蓄えられている。環境オブジェクトは暗黙の引数であり、関数呼び出しで自動的に渡される(関数の節を参照せよ)。
実行中に、関数の本体はその実行コンテキストを透過的に参照することができる。これは、単一の識別子はローカル変数または環境オブジェクトのスロットのどちらかから参照することができることを意味する。グローバル変数は特別な書式を求められる(変数の節を参照せよ)。
キーワードthisによって、環境オブジェクトに明示的にアクセスすることができる。
変数
Squirrelの変数には2つの型がある。ローカル変数とテーブル(または配列)のスロットである。グローバル変数はテーブルに蓄えられているため、これはテーブルスロットである。
単一の識別子は、ローカル変数または環境オブジェクトのスロットを参照する。
derefexp := id;
_table["foo"] _array[10]
テーブルには . を用いることもできる。
derefexp := exp '.' id
_table.foo
最初に、Squirrelは識別子がローカル変数かどうかをチェックする(なお、関数の引数はローカル変数である)。もしそうでないなら、次にそれが環境オブジェクト(this)のメンバかどうかをチェックする。
例えば、次の例ではローカル変数 a へアクセスして10が出力されるだろう。
function testy(arg) { local a = 10; print(a); return arg; }
次の例では、fooがthis.fooもしくはthis["foo"]として評価される。
function testy(arg) { local a = 10; return arg + foo; }
グローバル変数はルートテーブルと呼ばれるテーブルに蓄えられている。
通常、グローバルスコープにおいて、環境オブジェクトはルートテーブルである。
しかし、他のスコープからグローバルテーブルへ明示的にアクセスする場合には、
スロット名の前に :: を付ける必要がある(例えば::fooのように)。
exp:= '::' id
例えば、次の例ではグローバル変数fooへアクセスしている。
function testy(arg) { local a = 10; return arg + ::foo; }
しかし、Squirrel 2.0以降では、もし変数がローカルにもthisにも見付からないときには、Squirrelはルートテーブルを見に行くようになった。
function test() { foo = 10; }
上の例は次の例と等価である。
function test() { if ("foo" in this) { this.foo = 10; } else { ::foo = 10; } }
命令文
Squirrelのプログラムは単純な命令文のシーケンスである。
stats := stat [';'|'\n'] stats
Squirrelのおける命令文はC系の言語(C, C++, Java, C#など)と同程度であり、代入、関数呼び出し、プログラムフロー操作構造などからなる。
加えて、yieldやテーブルと配列のコンストラクタなどのようないくつかの特別な命令文がある(これらの全ての詳細は後述)。
命令文は改行もしくは ; によって区切られる(もしくswitch/case文中におけるcase文のキーワードやdefault)。もし、文の次が } なら、改行と ; も不要である。
ブロック
stat := '{' stats '}'
中括弧 { } によって区切られた命令文のシーケンスはブロックと呼ばれる。ブロックはそれ自体が命令文である。
制御命令
Squirrelはif, while, do-while, switch-case, for, foreachといったほとんどの一般的な制御命令を実装している。
trueとfalse
Squirrelは真偽値型を持つが、C++のようにnull、0(整数)、0.0(浮動小数点数)をfalseと見なし、他の値をtrueと見なす。
if/else
stat:= 'if' '(' exp ')' stat ['else' stat]
どちらの命令を実行するのかは式の結果次第である。
if (a > b) a = b; else b = a; //// if (a == 10) { b = a + b; return a; }
while
stat:= 'while' '(' exp ')' stat
条件がfalseとなるまで命令文を実行する。
function testy(n) { local a = 0; while (a < n) a += 1; while(1) { if (a < 0) break; a -= 1; } }
do/while
stat:= 'do' stat 'while' '(' expression ')'
命令文を一回実行し、その後は条件式がfalseとなるまでその命令文の実行を繰り返す。
local a = 0; do { print(a + "\n"); a += 1; } while (a > 100)
switch
stat := 'switch' ''( exp ')' '{' 'case' case_exp ':' stats ['default' ':' stats] '}'
その本体内のひとつのcase文に制御を渡すことで、複数のコードから選択することを可能にする制御命令である。
その制御はexpと一致するcase_expを持つラベルへ遷移する。一致したものがなければ、(もし存在するなら)defaultラベルへと飛ぶ。
switch命令は任意の数のcaseを含めることができる。同じ結果となる式を持つcaseが2つあるときは、最初のものが先に考慮される。
defaultラベルは必ずひとつで最後になければならない。break命令はswitchブロックを抜けるために用いられる。
ループ命令
for
stat:= 'for' '(' [initexp] ';' [condexp] ';' [incexp] ')' statement
条件がfalseとならない間、命令文を実行する。
for (local a = 0; a < 10; a += 1) print(a + "\n"); // or glob <- null for (glob = 0; glob < 10; glob += 1) { print(glob + "\n"); } // or for (;;){ print(loops forever + "\n"); }
foreach
'foreach' '(' [index_id','] value_id 'in' exp ')' stat
配列、テーブル、クラス、文字列、ジェネレータに含まれる全要素に対して命令文を実行する。
もし、expがジェネレータなら、それは(可能な間だけ)繰り返しの度に再開される。
その値はresumeの結果となり、添字はこの繰り返しの回数であり、0から開始する。
local a = [10, 23, 33, 41, 589, 56] foreach(idx, val in a) print("index=" + idx + " value=" + val + "\n"); // or foreach(val in a) print("value=" + val + "\n");
break
stat := 'break'
break命令はループ(for, foreach, while, do/while)の実行を終了させる。または、switch命令を抜ける。
continue
stat := 'continue'
continue命令は後に続く命令の実行をスキップして、ループの次の繰り返しへ進ませる。
return
stat:= return [exp]
return命令は現在の関数とジェネレータの実行を終了させる。このとき、式の結果を返すことができる。もし、式が省かれているなら、nullが返される。
ジェネレータ内部でreturn命令が使用されているときは、ジェネレータはもはや再開できなくなる。
yield
stat := yield [exp]
(ジェネレータの項を参照せよ).
ローカル変数宣言
initz := id [= exp][',' initz] stat := 'local' initz
ローカル変数はプログラムのどの箇所でも宣言することができる。それは宣言箇所から、それが宣言されたブロックの終りまで存在する。
例外: ローカル宣言はループの最初の式に行うこともできる。
for (local a = 0; a < 10; a += 1) print(a);
関数宣言
funcname := id ['::' id] stat:= 'function' id ['::' id]+ '(' args ')'[':' '(' args ')'] stat
新しい関数を作成する。
クラス宣言
memberdecl := id '=' exp [';'] | '[' exp ']' '=' exp [';'] | functionstat | 'constructor' functionexp stat:= 'class' derefexp ['extends' derefexp] '{' [memberdecl] '}'
新しいクラスを作成する。
try/catch
stat:= 'try' stat 'catch' '(' id ')' stat
try命令は実行時エラーやthrow命令などによって例外状態が発生するようなコードブロックを囲む。
catch節は例外操作コードを提供する。catch節が例外を捕らえたとき、その id には例外が割り付けられている。
throw
stat:= 'throw' exp
例外を投げる。どのような値を投げることもできる。
式
stat := exp
Squirrelのおいて、全ての式は命令文と見なすことができる。
このとき、式の結果は捨てられる。
式
代入(=)と新規スロット(<-)
exp := derefexp '=' exp exp:= derefexp '<-' exp
Squirrelは2種類の代入を実装している。ひとつめは( = による)通常の代入である。
a = 10;
ふたつめは「新規スロット」代入である。
a <- 10;
新規スロット代入は新規スロットをテーブルへ追加することができる(テーブルの項を参照せよ)。
もし、スロットがそのテーブルに既に存在しているなら、通常の代入と同じ動作となる。
演算子
?: 演算子
exp := exp_cond '?' exp1 ':' exp2
どちらの条件を評価するのかは式の結果次第である。
算術式
exp:= 'exp' op 'exp'
Squirrelは標準の算術演算子 +, -, *, /, % をサポートしている。
これ以外にも、簡略演算子(+=, -=, *=, /=, %=)、インクリメントとデクリメント(++, --)もサポートしている。
a += 2; // は次の式と同じである a = a + 2; x++ // は次の式と同じである x = x + 1
全ての演算子は整数と浮動小数点数に対して正常に機能する。もし、オペランドのひとつが整数でもうひとつが浮動小数点数なら、式の結果は浮動小数点数となる。
演算子 + は文字列では特別な振る舞いをする。もし、オペランドのひとつが文字列であるなら、演算子 + はもうひとつのオペランドを文字列に変換しようと試み、その後、両者を結合する。
インスタンスとテーブルに対しては、_tostringが呼び出される。
関係式
exp:= 'exp' op 'exp'
Squirrelにおける関係式は ==, <, <=, >, >=, != である。
これらの演算子は、式がfalseならnullを返し、式がtrueならnull以外の値を返す。
内部的には、VMはtrueとして整数の 1 を使用しているが、これは将来に変更されるかもしれない。
論理式
exp := exp op exp exp := '!' exp
Squirrelにおける論理式は &&, ||, ! である。
演算子 && (論理積)は最初の引数がnullならnullを返し、それ以外は2番目の引数を返す。演算子 || (論理和)は最初の引数がnullでないならその値を返し、それ以外は2番目の引数を返す。
演算子 ! は否定対象がnull以外の値ならnullを返し、対象がnullならnull以外の値を返す。
in 演算子
exp:= keyexp 'in' tableexp
テーブルのスロットが存在するかどうかをテストする。もし、keyexpがtableexpに存在するなら、null以外を返す。
local t= { foo = "I'm foo", [123] = "I'm not foo" }
if ("foo" in t) dostuff("yep"); if (123 in t) dostuff();
instanceof 演算子
exp:= instanceexp 'instanceof' classexp
クラスインスタンスが特定のクラスのインスタンスであるかどうかをテストする。もし、instanceexpがclassexpのインスタンスであるなら、null以外を返す。
typeof 演算子
exp:= 'typeof' exp
型名を文字列で返す。
local a = {}, b = "squirrel" print(typeof a); // "table"が表示される print(typeof b); // "string"が表示される
コンマ演算子
exp:= exp ',' exp
コンマ演算子は2つの式を左から右に順番に評価する。
この演算結果は右の式の結果であり、左の式の結果は無視される。
local j = 0, k = 0; for(local i = 0; i < 10; i++, j++) { k = i + j; } local a, k; a = (k = 1, k + 2); // aは3になる
ビット操作演算子
exp:= 'exp' op 'exp' exp := '~' exp
Squirrelは標準のC風のビット操作演算子 &, |, ^, ~, <<, >> に加えて、論理シフト演算子 >>> をサポートしている。論理シフト演算子 >>> は通常のシフト演算子 >> のように機能するが、左のオペランドを符号なし整数として扱うため、符号の影響を受けない。
これらの演算子は整数値にのみ機能し、他の型を渡したときは例外が発生する。
演算子の優先順位
演算子 | 優先度 |
-, ~, !, typeof, ++, -- | 低い |
/, *, %, ... | |
+, - | |
<<, >>, >>> | |
<, <=, >, >= | |
==, != | |
& | |
^ | |
|| | |
&&, in | |
||| | |
?: | |
+=, =, -=, ... | |
,(comma operator) | 高い |
テーブルコンストラクタ
tslots := ( 'id' '=' exp | '[' exp ']' '=' exp ) [','] exp := '{' [tslots] '}'
新しいテーブルを作成する。
local a = {} // 空テーブルを作成する
テーブルコンストラクタでスロット宣言を含めることもできる。これは次の書式を取る。
id = exp [',']
キーとしてidを、値としてexpを取る新規スロットを作成する。
local a = { slot1 = "I'm the slot value" }
別の書式として次のようなものがある。
'[' exp1 ']' = exp2 [',']
キーとしてexp1を、値としてexp2を取る新規スロットを作成する。
local a= { [1] = "I'm the value" }
この2つの書式を混ぜて使用することができる。
local table = { a = 10, b = "string", [10] = {}, function bau(a, b) { return a + b; } }
スロット宣言の間にあるコンマはオプションである。
委譲
exp:= 'delegate' parentexp : exp
テーブルの親をセットする。parentexpの結果にexpの式の結果の親をセットする(委譲の項を参照せよ)。
クローン
exp:= 'clone' exp
クローンはテーブル、配列、クラスインスタンスの浅いコピーを行う(新しいオブジェクトに全スロットをコピーするが、再帰的には処理しない)。
もし、元のテーブルが委譲を持っているなら、(コピーされずに)同じ委譲が委譲として新しいテーブルに代入される(委譲の項を参照せよ)。
クローンが完了した直後に、メタメソッド_clonedが呼び出される(メタメソッドの節を参照せよ)。
クラスインスタンスがクローンされたときは、コンストラクタは呼び出されない(代わりに、_clonedで初期化を行う必要がある)。
配列コンストラクタ
exp := '[' [explist] ']'
新しい配列を作成する。
a <- [] // 空の配列を作成
配列はコンストラクト時に引数付きで初期化することもできる。
a <- [1, "string!", [], {}] // 4要素で配列を作成
テーブル
テーブルは、キーと値のペア(スロットと呼ばれる)として実装された連想コンテナである。値としては任意の型を用いることができ、キーとしてはnull以外の型を用いることができる。
テーブルはSquirrelの骨格であり、委譲とその他の多くの機能はこの型を通して実装されている。グローバル変数を蓄えている環境でさえテーブルである(これはルートテーブルとして知られている)。
コンストラクト
テーブルはテーブルコンストラクタを通して作成される(テーブルコンストラクタの項を参照せよ)。
スロットの作成
存在しているテーブルへの新規スロットの追加は「新規スロット」演算子 <- を用いて行われる。この演算子は通常の演算子と同様に機能するが、スロットが存在しないときはそれを作成する。
local a = {}
このとき、次の例は例外が発生する。
なぜなら、newslotと呼ばれるスロットがテーブル a には存在していないからである。
a.newslot = 1234
次の例は成功するだろう。
a.newslot <- 1234;
または、
a[1] <- "I'm the value of the new slot";
スロットの削除
exp:= delete derefexp
スロットの削除はキーワードdeleteによって行われる。
この式の結果は削除されたスロットの値となる。
a <- { test1 = 1234 deleteme = "now" }
delete a.test1 print(delete a.deleteme); // この結果は文字列 "now" となる
配列
配列は値のシーケンスであり、添字として0から(配列のサイズ - 1)までの整数値を取る。配列の要素はその添字を通して得ることができる。
local a = ["I'm a string", 123] print(typeof a[0]) // "string"と表示される print(typeof a[1]) // integer"と表示される
配列とその要素のリサイズ、挿入、削除などは標準関数を通じて行われる(組み込み関数の節を参照せよ)。
関数
関数は整数や文字列と同様にファーストクラスオブジェクトであり、テーブルスロット、ローカル変数、配列に代入可能であり、関数に引数として渡すことができる。
関数はSquirrel上もしくは(ANSI C互換の呼び出し規約によって)ネイティブ言語で実装することができる。
関数宣言
関数は関数式を通して宣言される。
local a = function(a, b, c) {return a + b - c;}
シンタックスシュガーを用いて、次のように宣言することもできる。
function ciao(a, b, c) { return a + b - c; }
また、次のコードと等価である。
this.ciao <- function(a, b) { return a + b - c; }
このような宣言をすることも可能である。
T <- {} function T::ciao(a, b, c) { return a + b - c; }
// これは次のように書ける
T.ciao <- function(a, b, c) { return a + b - c; }
// or
T <- { function ciao(a, b, c) { return a + b - c; } }
Squirrelの関数は可変長引数の関数を用いることができる。
可変長引数の関数は3つのドット(...)を引数リストの最後に追加することで宣言される。
関数が呼び出されたとき、全ての余分の引数はvargvと呼ばれる疑似配列を通してアクセスすることができる。
vargvで用いることができる添字は数値(整数もしくは浮動小数点数)のみである。vargvが持つ引数の数は疑似変数vargcに代入されている。
vargvは実オブジェクトではないので、代入や引数として渡すことができないことに注意せよ。
function test(a, b, ...) { for(local i = 0; i < vargc; i++) { ::print("varparam " + i + " = " + vargv[i] + "\n"); } }
test("goes in a", "goes in b", 0, 1, 2, 3, 4, 5, 6, 7, 8);
関数呼び出し
exp:= derefexp '(' explist ')'
この式はexplis、derefexpの順で評価されて最後に呼び出しが行われる。
Squirrelにおける全ての関数呼び出しは、隠しパラメータとして環境オブジェクトthisが呼び出された関数に渡される。
このパラメータthisはオブジェクトであり、関数はそのオブジェクトから参照可能である。
もし次のような書式で関数を呼び出した場合、fooに渡される環境オブジェクトはtableとなる。
table.foo(a)
foo(x, y) // this.foo(x, y)と等価
このとき、環境オブジェクトはthisとなる(呼び出し元の関数と同じ)。
環境の関数への束縛
デフォルトではSquirrelの関数呼び出しでは環境オブジェクトとしてthisを渡す。関数はそのオブジェクトから参照可能である。しかし、組み込みメソッドclosure.bindenv(env_obj)を用いることで、クロージャへの静的な環境の束縛をすることができる。
メソッドbindenv()はそれに割り付けられた環境とともに、クロージャの新しいインスタンスを返す。環境オブジェクトが関数に束縛されると、その関数が呼び出されたときには常にそのthisパラメータは前もって束縛していた環境となる。
このメカニズムはC#の委譲に類似したコールバック系を実装するために有用である。
注意
クロージャは環境オブジェクトを束縛するために弱参照を保持する。
このため、オブジェクトが削除された場合、次回のクロージャへの呼び出しでは環境オブジェクトがnullとなる。
自由変数
自由変数は、その関数スコープ内では見えないが、関数によって参照される変数である。次の例において、関数foo()はxとyとtestyを自由変数として宣言している。
local x = 10, y = 20 testy <- "I'm testy"
function foo(a, b):(x, y, testy) { ::print(testy); return a + b + x + y; }
関数作成時に、自由変数の値は凍結され、関数に割り付けられる。それが呼ばれる度に、その値は暗黙のパラメータとして関数に渡される。
末尾再帰
末尾再帰は部分的にプログラムの再帰を繰り返しに変更するための手法である。
ある関数の再帰的呼び出しがその関数の最後の実行命令文であったとき(returnの直前)、末尾再帰が適用される。もし適用されたなら、再帰を呼び出し前に、Squirrelインタプリタは呼び出し元のスタックフレームを崩す。
これによって、スタックオーバーフローの危険なしに、非常に深い再帰をすることが可能となる。
function loopy(n) { if (n > 0){ ::print("n=" + n + "\n"); return loopy(n - 1); } }
loopy(1000);
クラス
SquirrelはJavaやC++などの言語と同様のクラスメカニズムを実装している。しかし、その動的性質により、いくつかの性質が異なる。
クラスは整数や文字列と同様にファーストクラスオブジェクトであり、テーブルスロット、ローカル変数、配列に代入可能であり、関数に引数として渡すことができる。
クラス宣言
クラスオブジェクトはキーワードclassを通して作成される。クラスオブジェクトはテーブルの宣言と同じ書式に従う(テーブルの節を参照せよ)。唯一の違いは、セパレータに , だけでなく ; も使用できることである。
例えば、次の通り。
class Foo { // constructor constructor(a) { testy = ["stuff", 1, 2, 3]; } // member function function PrintTesty() { foreach(i, val in testy) { ::print("idx = " + i + " = " + val + " \n"); } } // property testy = null; }
上の例は、次の例のシンタックスシュガーである。
Foo <- class { // constructor constructor(a) { testy = ["stuff", 1, 2, 3]; testy = a; } // member function function PrintTesty() { foreach(i, val in testy) { ::print("idx = " + i + " = " + val + " \n"); } } // property testy = null; }
名前空間を模倣するために、次のように宣言することもできる。
// just 2 regular nested tables FakeNamespace <- { Utils = {} }
class FakeNamespace.Utils.SuperClass { constructor() { ::print("FakeNamespace.Utils.SuperClass") } function DoSomething() { ::print("DoSomething()") } }
function FakeNamespace::Utils::SuperClass::DoSomethingElse() { ::print("FakeNamespace::Utils::SuperClass::DoSomethingElse()") }
local testy = FakeNamespace.Utils.SuperClass(); testy.DoSomething(); testy.DoSomethingElse();
宣言後にも、メソッドとプロパティはテーブルと同様に(演算子 <- と = によって)、追加と変更ができる。
// 新しいプロパティを追加 Foo.stuff <- 10;
// 既に存在しているプロパティのデフォルトの値を変更 Foo.testy = "これは文字列";
// 新しいメソッドを追加 function Foo::DoSomething(a,b) { return a + b; }
クラスがインスタンス化された後は、プロパティとメソッドをそのクラスへ新たに追加することはできなくなる。
静的変数
Squirrelのクラスは静的メンバ変数をサポートしている。静的変数は、全てのクラスインスタンス間でその値を共有する。
静的変数はその変数宣言の前にキーワードstaticを付けて宣言する。その宣言はクラス本体で行わなければならない。
Note
静的変数は読み込み専用である。
class Foo { constructor() { //..stuff } name = "通常の変数"; // 静的変数 static classname = "クラス名はFoo"; };
クラス属性値
クラスはそのメンバに属性値を関連付けることができる。属性値はメタデータの一種であり、文書文字列やIDEのプロパティやコード生成器などのような
アプリケーションに特化した情報を蓄えるために用いることができる。クラス属性値は、クラス本体のメンバ宣言の前に、シンボル </ />によって区切られて宣言される。
例を次に示す。
class Foo </ test = "これはクラスレベルの属性値" />{ </ test = "なんとなくな属性値" /> // PrintTestyの属性値 function PrintTesty() { foreach(i, val in testy) { ::print("idx = " + i + " = " + val + " \n"); } } </ flippy = 10, second = [1, 2, 3] /> // testyの属性値 testy = null; }
実際には、クラス属性値はテーブルである。Squirrelは可読性の向上させるために、属性値の宣言を中括弧 { } のかわりに </ /> を用いる。
これは、テーブルに適用される全てのルールは、属性値にも適用できることを意味する。
属性値は組み込み関数classobj.getattributes(membername)によって探索することができる(組み込み関数の節を参照せよ)。また、classobj.setattributes(membername,val)によって追加と変更を行うことができる。
次のコードはFooのメンバの属性値を全て出力させる。
foreach(member, val in Foo) { ::print(member + "\n"); local attr; if((attr = Foo.getattributes(member)) != null) { foreach(i, v in attr) { ::print("\t" + i + " = " + (typeof v) + "\n"); } } else { ::print("\t<属性値がない>\n") } }
クラスインスタンス
クラスオブジェクトはいくつかのテーブルの機能を受け継いでいるが、同じクラスの複数のインスタンスを作成できるという違いがある。
クラスインスタンスはそれを作成したテーブルと同じ構造を共有するが、独自の値を持つこともできるオブジェクトである。クラスのインスタンス化は関数の記法を用いるので、クラスインスタンスはクラスオブジェクトを呼び出すことで作成される。
クラスインスタンスを返す関数のようなクラスを想像すると便利である(?)。
// Fooのインスタンスを新たに作成 local inst = Foo();
クラスインスタンスが作成されたとき、そのメンバはクラス宣言で指定された値で初期化される。
クラスがコンストラクタと呼ばれるメソッドを定義したとき、クラスのインスタンス化時には自動的にそれが呼び出される。
コンストラクタメソッドは引数を持つことができる。これは、インスタンス化が要求されたときの引数の数に影響する。
通常の関数と同様に、コンストラクタは(パラメータ ... によって)可変長引数を扱うことができる。
class Rect { constructor(w, h) { width = w; height = h; } x = 0; y = 0; width = null; height = null; }
// Rectのコンストラクタは2つの引数を持つので、クラスは2引数で「呼び出す」必要がある。 local rc = Rect(100, 100);
インスタンスが作成された後、テーブルに適用されているルールに従って、そのプロパティに代入やフェッチすることができる。メソッドはセットできない。
インスタンスのメンバは削除できない。
インスタンスを作成したクラスオブジェクトは組み込み関数instance.getclass()を通して検索することができる(組み込み関数の節を参照せよ)
演算子instanceofはクラスインスタンスが特定のクラスのインスタンスかどうかをテストする。
local rc = Rect(100, 100); if(rc instanceof ::Rect) { ::print("It's a rect"); } else { ::print("It isn't a rect"); }
継承
Squirrelのクラスは単一継承をサポートしている。
これは、クラス定義において、キーワードextendsを式の後に追加することで行う。
導出クラスの文法は次のようになる。
class SuperFoo extends Foo { function DoSomething() { ::print("I'm doing something"); } }
導出クラスが宣言されたとき、Squirrelはまず全ての基底クラスのメンバを新しいクラスにコピーし、次に宣言の残りの評価を行う。
導出クラスはその基底クラスから全てのメンバとプロパティを継承している。導出クラスが基底関数をオーバーライドした場合、基底クラスの実装は隠蔽される。
基底クラスオブジェクトからそのメソッドへフェッチすることで、基底クラスのオーバーライドされたメソッドへアクセスすることは可能である。
例を次に示す。
class Foo { function DoSomething() { ::print("I'm the base"); } };
class SuperFoo extends Foo { // overridden method function DoSomething() { // calls the base method ::Foo.DoSomething(); ::print("I'm doing something"); } }
コンストラクタに対しても同様のルールが適用される。コンストラクタは正規の関数である(構築時に自動的に呼び出される点が異なるが)。
class Base { constructor() { ::print("Base constructor\n"); } }
class Child extends Base { constructor() { ::Base.constructor(); ::print("Child constructor\n"); } }
local test = Child();
基底クラスはキーワードparentによって調べることができる。
parentは「疑似スロット」であり、スロットparentにセットすることはできない。
local thebaseclass = SuperFoo.parent;
メソッドは特別の保護ポリシーを持たない。そのため、同じオブジェクトのメソッドを呼び出すとき、クラス内の別メソッドを呼ぶような基底クラスのメソッドがあったとしても、導出クラスのオーバーライドしているメソッドが呼ばれるだろう。
class Foo { function DoSomething() { ::print("I'm the base"); } function DoIt() { DoSomething(); } };
class SuperFoo extends Foo { // overridden method function DoSomething() { ::print("I'm the derived"); } function DoIt() { ::Foo.DoIt(); } }
// creates a new instance of SuperFoo local inst = SuperFoo();
// prints "I'm the derived" inst.DoIt();
メタメソッド
クラスインスタンスはメタメソッドによって言語の意味的な様相をカスタマイズすることができる(メタメソッドの節を参照せよ)。C++プログラマにとって、メタメソッドはおおまかに言うと演算子オーバーロードのように振る舞うものである。
クラスが使用できるメタメソッドは_add, _sub, _mul, _div, _unm, _modulo, _set, _get, _typeof, _nexti, _cmp, _call, _delslot, _tostringである。
クラスオブジェクトは、_newmemberと_inheritedという2つのメタメソッドのみが使用できる。
メタメソッド_addを実装したクラスを作成する例を次に示す。
class Vector3 { constructor(...) { if(vargc >= 3) { x = vargv[0]; y = vargv[1]; z = vargv[2]; } } function _add(other) { return ::Vector3(x + other.x, y + other.y, z + other.z); }
x = 0; y = 0; z = 0; }
local v0 = Vector3(1, 2, 3) local v1 = Vector3(11, 12, 13) local v2 = v0 + v1; ::print(v2.x + "," + v2.y + "," + v2.z + "\n");
2.1以降は、クラスは_inheritedと_newmember2つのメタメソッドを新たにサポートする。
_inheritedは_inheritedを実装したクラスを継承したときに呼び出される。
_newmemberは(宣言時に)クラスにメンバが追加されたときに呼び出される。
ジェネレータ
yield命令を含む関数は「ジェネレータ関数」と呼ばれる。
ジェネレータ関数が呼ばれたとき、それは関数本体を実行するかわりに、中断状態にあるジェネレータを新たに返す。返されたジェネレータは(それが生存している間は)resume命令によって再開することができる。
yieldキーワードはジェネレータの実行を中断させる。このとき、ジェネレータを再開させた関数へ、式の結果を返すことができる。明示的なreturn命令と関数本体の末尾による終了で、ジェネレータは死亡する。
また、ジェネレータ実行中に取り扱えない例外が発生したときも、ジェネレータは自動的に死亡する。死亡したジェネレータはもはや再開することはできない。
function geny(n) { for (local i = 0; i < n; i += 1) yield i; return null; }
local gtor = geny(10); local x; while (null != (x = resume gtor)) print(x + "\n");
このプログラムの出力結果は次のようになる。
0 1 2 3 4 5 6 7 8 9