07. 変数 編
今回は LuneScript の変数について説明します。
変数
LuneScript は静的型付け言語であり、変数は型を持ちます。
変数は、次のように let で宣言します。
// @lnsFront: ok
let val:int = 1;
上記の例は、初期値として int の 1 を持つ変数 val を宣言しています。
変数名の後には、型を指定します。
なお、初期値が int の 1 ではなく、 real の 1.0 をセットすると、型が違うためコンパイルエラーになります。
// @lnsFront: error
let val:int = 1.0; // error
また、現在は変数宣言には初期値が必須です。
これは、未初期化変数へのアクセスを防ぐためです。
将来的には、変数が値を保持しているかどうかをフロー解析で判断できるようにして、
初期値不要にすることを考えています。
変数宣言時の初期化は必須ではありません。 なお、初期化していない変数を参照した場合、コンパイルエラーになります。 詳しくは後述します。
また、変数宣言時に初期化しない場合でも、型推論は可能です。
型推論
LuneScript は型推論をサポートしています。
変数にセットする初期値から、その変数の型を決定できます。 これによって、次のように型を指定せずに変数を宣言できます。
// @lnsFront: ok
let val1 = 1; // int
let val2 = 1.0; // real
let val3 = "abc"; // str
この場合、 val1 は int, val2 は real, val3 は str であるとして処理します。
型を明示する必要があるのは、次の場合などです。
-
nilable 型の変数の初期値に nil を設定する
let mut val:int! = nil;
-
リスト型や、マップ型の変数の初期値に、 immediate な空の値 (
[]
,{}
など)を設定するlet mut val:List<int> = [];
-
次のようなクラス型の変数にサブクラスのインスタンスを設定する際、変数の型をスーパークラスの型としたい場合
let val:Super = new Sub();
変数の初期化
初期化していない変数を参照すると、コンパイルエラーになります。
// @lnsFront: error
{
let val;
print( "%s" ( val ) ); // error
}
上記の print()
では、未初期化の val にアクセスしていますが、
ここでコンパイルエラーになります。
フロー解析
変数初期化は、フローを解析して変数未初期化のパスがないかチェックします。
例えば次の場合、エラーになります。
// @lnsFront: error
fn func( flag:bool )
{
let val;
if flag {
val = 1;
}
print( val ); // error
}
上記エラーの原因は、 flag が true の場合は val が初期化されますが、 false の場合は val が初期化されないためです。
次のように、アクセスする前に全てのパスで初期化をする必要があります。
// @lnsFront: ok
fn func( flag:bool )
{
let val;
if flag {
val = 1;
}
else {
val = 2;
}
print( val ); // ok
}
なおこの処理は、 変数 val に対する初期化であり、 val に対する書き換えではないため、 後述する mut 宣言の必要はありません。
ちなみに次のような少し複雑な場合も、フローを解析します。
// @lnsFront: error
fn func( kind:int )
{
let val;
if kind < 10 {
if kind > 0 {
val = 1;
}
else {
if kind == 0 {
val = 2;
}
elseif kind == 1 {
val = 3;
}
// ※
}
}
else {
val = 4;
}
print( val ); // error
}
少し分かり難いと思いますが、 上記 ※ の位置で else の時に val の初期化が抜けているため、print の val 参照がエラーとなります。
なお、次の場合も print( val ) の箇所でエラーになります。
// @lnsFront: error
fn func( flag:bool )
{
let val;
fn sub() {
print( val ); // error
}
val = 1;
sub();
}
本来は sub()
を実行する時には val が初期化されるので、
エラーになるべきではないですが、これは現在の仕様です。
型推論
変数宣言時に初期化しない場合も、型推論は可能です。
ただし、型推論はフロー解析で最初に代入された型が使用されます。
例えば次の場合は、
// @lnsFront: error
fn func( flag:bool )
{
let val;
if flag {
val = 1;
}
else {
val = 1.0; // error
}
}
最初の val = 1
で val は int 型になります。
そして、次の val = 1.0
の時には、
int 型の val に real の 1.0 を代入しようとしているため、
エラーとなります。
上記のような場合は、次のように変数宣言時に型を宣言することで、 エラーを回避できます。
// @lnsFront: ok
fn func( flag:bool )
{
let val:stem; // stem 型を宣言
if flag {
val = 1;
}
else {
val = "a";
}
print( val );
}
stem 以外にも、型を宣言しなければならないケースはあります。 例えばスーパークラスの型の変数を利用したい場合や、 nilable 型の変数を利用したい場合などがあります。
shadowing
LuneScript では、 同名の変数宣言を禁止しています。
ここで同名とは、同一スコープ内での同名に限らず、 アクセス可能なスコープ内での同名を指します。
具体的には、次の変数宣言はエラーとなります。
// @lnsFront: error
{
let val = 1;
{
let val = 1; // error
}
}
この仕様は賛否別れると思いますが、安全側に振ってこの仕様にしています。
アクセス制御
宣言した変数は、ローカル変数として処理されます。
外部のモジュールに公開したい場合は、次のように pub を付加して宣言します。
// @lnsFront: ok
pub let val = 1;
外部公開されている変数にアクセスする場合、次のように import を使用します。
// @lnsFront: skip
import SubModule;
print( SubModule.val );
ここで、
SubModule は pub let val = 1;
を宣言している LuneScript のモジュール(SubModule.lns)です。
この val にアクセスする場合、SubModule.val とすることでアクセスできます。
変数を外部モジュールに公開する方法としては pub が基本ですが、 もう一つ global を使うことも出来ます。
// @lnsFront: ok
pub let val1 = 1;
global let val2 = 2;
pub と global の違いは、 名前空間の違いです。
次の例を見ると分かり易いと思いますが、 これは上記 val1, val2 を外部からアクセスしているサンプルです。
// @lnsFront: skip
import SubModule;
print( SubModule.val1 );
print( val2 );
val1 は、 SubModule.val1 として SubModule の名前空間内の変数としてアクセスしますが、 val2 は、最上位の名前空間の変数としてアクセスします。
LuneScript だけでシステムを開発する際は、 global を利用することはまず無いと思います(というか global の使用を避けるべきです)が、 他の Lua モジュールと連携して処理する際は、 global を使用せざるを得ないこともあると思います。
そのような互換性を保つことを目的として、 global をサポートしています。
なお global の制約として、次があります。
「global 宣言した変数は、 その変数を宣言したモジュールを import した時に有効になる。」
例えば次の例では val2 は、 SubModule になんの関係もなく存在しているように見えますが、
// @lnsFront: skip
import SubModule;
print( SubModule.val1 );
print( val2 );
次の場合 SubModule を import していないため、val2 は存在しないのでエラーとなります。
// @lnsFront: skip
print( val2 );
また外部公開する変数には、次の制約があります。
「外部公開する変数は、スクリプトの最上位のスコープに宣言しなければならない」
例えば、次の val2 は最上位のスコープではないためエラーとなります。
// @lnsFront: error
pub let val = 1;
{
pub let val2 = 1; // error
}
mutable
単に宣言した変数は、変更禁止の変数として扱います。
例えば、次の val = 2
はエラーとなります。
// @lnsFront: error
let val = 1;
val = 2; // error
可変な変数(mutable)とする場合、次のように mut で宣言します。
// @lnsFront: ok
let mut val = 1;
val = 2;
なお、次のように immutable の変数を宣言した後に初期値を代入することもできます。
// @lnsFront: ok
let val;
val = 1;
ただし、次のように初期値を代入した後にさらに値をセットするとエラーになります。
// @lnsFront: error
let val;
val = 1;
val = 2; // error
immutable な型
上記の通り、mut 宣言しない変数は immutable になります。 さらに mut 宣言せずに 型推論された型 も immutable になります。 例えば次の場合、 list1 は mut 宣言しているため List の変更操作(insert)が可能ですが、 list2 は mut 宣言せずに immutable であるため List の変更操作はエラーになります。
// @lnsFront: error
let mut list1 = [1];
list1.insert( 2 ); // ok
let list2 = [1];
list2.insert( 2 ); // error
immutable な型は、元の型 T に & を付けて &T として表記します。
例えば &List<int>
は、 変更操作できないリスト List<int> を表します。
なお、変更操作は出来ませんが、 foreach などの参照操作は出来ます。
&List<List<int>>
は、 List<int>
を要素に持つ immutable なリストです。
ここで List<int>
は & が付いていないため mutable です。
つまり、次のようになります。
// @lnsFront: error
let list:&List<List<int>> = [[100],[]];
list[1].insert( 1 ); // ok
list.insert( [10] ); // error
型推論と mutable
前述の通り、 mut 宣言していない変数の型は immutable になります。
ただし、これは型推論を利用した場合です。
mut 宣言していない変数でも、型を明示している場合は、 その型の mutable 宣言に依存します。
例えば以下の場合、
// @lnsFront: error
let list1:List<int> = [1,2];
let list2:&List<int> = [1,2];
let mut list3 = [1,2];
let list4 = [1,2];
list1.insert( 3 );
list2.insert( 3 ); // error
list3.insert( 3 );
list4.insert( 3 ); // error
list2 と list4 が immutable な &List<int> になるため、
list2.insert( 3 );
と list4.insert( 3 );
がエラーになります。
なお、以前この仕様は不具合があり、ver 1.2.0 で修正しています。 以前は、型を明示した場合も mut 宣言しない場合は immutable な型になっていましたが、 その挙動が変数、メンバ、引数で劣なっていたため、 現状の仕様に修正しています。
もしも ver 1.2.0 以前の仕様に戻したい場合、 オプション –legacy-mutable-control を指定してください。
ただし、このオプションは将来廃止する可能性があります。
複数宣言
LuneScript は、 Lua と同じで関数の戻り値に複数の値を返せます。
この戻り値を変数宣言の初期値とするには、次のように宣言します。
// @lnsFront: skip
let val1, val2 = func();
let mut val3, mut val4 = func();
mut は各変数名の前に宣言します。
アクセスチェック
宣言したローカル変数に対し、 値を設定した後にその変数を参照しないと、警告を出力します。 一方で、クラスのメンバや、関数の引数などはアクセスチェックの対象になりません。
次のサンプルは、 多値返却の 1 番目の値を使用せずに 2 番目の値だけを使用する場合の例です。 この場合、1 番目の値を格納している val1 が使用されていないことを警告します。
// @lnsFront: ok
fn sub(): int, int {
return 1, 2;
}
fn func() {
let val1, val2 = sub(); // warning val1
print( val2 );
}
このような多値返却の 2 番目以降の値にアクセスするためだけに宣言した変数に対して、 警告を出さないようにするには、次のように '_' シンボルを使用します。
// @lnsFront: ok
fn sub(): int, int {
return 1, 2;
}
fn func() {
let _, val2 = sub(); // ok
print( val2 );
}
なお、 '_' シンボルで宣言した変数にはアクセスできません。 アクセスするとエラーになります。
// @lnsFront: error
fn sub(): int, int {
return 1, 2;
}
fn func() {
let _, val2 = sub();
print( _ ); // error
print( val2 );
}
アクセスチェックは、変数の値を更新した後にも行なう。
例えば次の場合、 val1 は警告される。
// @lnsFront: ok
fn func() {
let mut val1 = 1;
print( val1 );
val1 = 2; // warning
}
これは、 val1 に 1 をセット後に print( val1 ) で val1 を参照しているが、
次に val1 = 2
で val1 を更新後に val1 を参照していないためである。
クロージャのアクセスチェック
このアクセスチェックはクロージャでも動作する。
次のサンプルは val1 = 2
後、
sub()
のコールがあることで val1 を参照していると判断して警告しない。
// @lnsFront: ok
fn func() {
let mut val1 = 1;
fn sub() {
print( val1 );
}
val1 = 2;
sub();
}
ただし、次の制限がある。
-
クロージャの関数コールではなく、参照した時点で、値の参照があったものとして処理する
- 例えば、クロージャ関数を変数に代入したり、別の関数の引数に渡した時点で処理する。
-
クロージャによるアクセスは参照、設定を区別しない
- クロージャ関数内で設定しかしていない場合も参照として扱う。
クロージャのアクセスチェックについては実験的な機能である。
特殊シンボル
次のシンボルは、特殊な値を指します。
シンボル | 値 |
---|---|
__mod__ |
モジュール名 |
__func__ |
現在の関数名 |
__line__ |
現在の行番号 |
なお、 __mod__
, __func__
が展開する名前の書式は、
将来変更する可能性 があります。
型変換(キャスト)
nil 以外の全ての値は、 stem 型の変数に代入できます。
これには、暗黙的な型変換が行なわれています。
// @lnsFront: ok
let mut val:stem = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];
一方、 stem 型の値から異なる型への代入はエラーします。
// @lnsFront: error
let val1:stem = 1;
let val2:int = val1; // error
明示的な型変換が必要な場合は、次の記事を参照してください。
参照
変数は、 一部(int,real,nil)を除いてオブジェクトの参照を保持します。
例えば次の場合、
// @lnsFront: ok
let mut list1 = [ 10 ];
let list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
foreach val in list2 {
print( val ); // 10 20 30
}
- list1 に List<int> 型のリスト (
[ 10 ]
)オブジェクトの参照をセット - list2 に list1 が保持する参照をセット
- list1 が参照するリストオブジェクトに 20, 30 を insert
- list2 が参照するリストオブジェクトの各値を
print()
ここで、 list1 と list2 は 同じリストオブジェクト を参照しているため、 list1 に 20, 30 を insert すると、 list2 を foreach した print( val ) は 10 20 30 を出力します。
また、次のように list2 に 40 を insert した場合、 同じリストオブジェクト に 40 を挿入するため、 print( val ) は 10 20 30 40 を出力します。
// @lnsFront: ok
let mut list1 = [ 10 ];
let mut list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
list2.insert( 40 );
foreach val in list2 {
print( val ); // 10 20 30 40
}
list1 に新しいリストオブジェクト ([ 100]
) をセットした場合、
list1 が参照するリストオブジェクトと、
list2 が参照するリストオブジェクトは異なるため、
print( val ) は 10 20 30 40 を出力します。
// @lnsFront: ok
let mut list1 = [ 10 ];
let mut list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
list2.insert( 40 );
list1 = [ 100 ];
foreach val in list2 {
print( val ); // 10 20 30 40
}
これは、 List<List<int>> の場合も同じです。
// @lnsFront: ok
let mut list = [ 10, 20 ];
let mut wrapList:List<List<int>> = [];
wrapList.insert( list );
wrapList.insert( list );
wrapList.insert( [ 100, 200 ] );
list[ 1 ] = list[ 1 ] + 1;
print( wrapList[ 1 ][ 1 ], wrapList[ 1 ][ 2 ] ); // 11 20
print( wrapList[ 2 ][ 1 ], wrapList[ 2 ][ 2 ] ); // 11 20
print( wrapList[ 3 ][ 1 ], wrapList[ 3 ][ 2 ] ); // 100 200
wrapList の 1, 2 番目に list を追加し、 wrapList の 3 番目に新しいリストオブジェクトを追加している。 その後 list[1]をインクリメント後、wrapList の中身を出力する。
ここで、 wrapList[1][1] と wrapList[2][1] は、同じ list[1] を指すため、 インクリメントされた値が出力される。 wrapList[3] は新しいリストオブジェクトになるため、インクリメントの影響はない。
まとめ
LuneScript の変数には、次の要素を取り入れています。
- 型推論
- アクセス制御
- mutable
- 複数宣言
Lua を静的片付けで扱う際に必要となる、最低限の機能を満しているつもりです。
次回は、 LuneScript の分岐制御について説明します。