11. nilable 編
今回は LuneScript の nilable について説明します。
nilable とは
LuneScript は、値に nil を持ちます。
以前説明しましたが、 stem 型の変数は nil 以外の全ての型を保持できます。
// @lnsFront: error
let mut val:stem = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];
val = nil; // error
では、どうすれば nil を保持できるのかというと、 stem ではなく stem! を使用します。
// @lnsFront: ok
let mut val:stem! = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];
val = nil; // ok
このように、 nil を保持可能な型を nilable と言います。
nilable は stem! だけでなく、 一部を除く全ての型に nilable 型が存在します。
例えば int! や str! は、 int の nilable 型、 str の nilable 型となります。
型名の末尾に ! を付加することで、 本来の型と nil を保持可能な nilable 型となります。
// @lnsFront: error
let mut val1:int = 1;
val1 = nil; // error
let mut val2:int! = 1;
val2 = nil; // ok
nil は、他のどの値とも異なる値で、異常値として利用するのに便利な値です。 しかし、 意図しないタイミングで変数の値が nil になることで、 不具合の原因となることが多くあります。
LuneScript は、 nil を保持出来る nilable 型と、 nil を保持できない非 nilable 型に分けることで、 nil 安全(NULL 安全)を実現しています。
nilable 型の制限
nilable 型は、そのままでは元の型として使用できないという制限があります。
この説明だと何のことだか伝わり難いと思いますので、次の例を見てください。
// @lnsFront: error
let val:int! = 1;
print( val + 1 ); // error
上記の val は int の nilable 型です。 そして val + 1 を実行していますが、これはエラーとなります。
なぜならば、 val は int ではなく int! なので、そのままでは int としては扱えないためです。
では、なぜ nilable にこのような制限があるかというと、次の通りです。
- nilable は nil を保持できる型
- つまり、nilable 型は nil である可能性がある
- 元の型として利用するには、 nil でないことを確認しなければならない
非 nilable の型には nil を代入出来ません。 そして、 nilable 型はそのままで元の型として利用が出来ません。
この制限によって、 ある変数が意図しないタイミングで nil になり、 不具合となることを論理的に防止することが出来ます。
これが多くの言語で取り入れられている nil 安全(NULL安全)の原理です。
nilable 型との比較
前述している通り、nilable 型は、そのままでは非 nilable として利用できません。
しかし、次のように比較すること出来ます。
// @lnsFront: ok
fn check( val:int! ) {
if val == 1 {
print( "ok" );
}
else {
print( "ng" );
}
}
check( 1 ); // ok
check( 2 ); // ng
上記サンプルでは val は int! で、 val == 1 で int と比較しています。 このように nilable と 非 nilable を比較することは可能です。
nilable 型から非 nilable 型への変換
次のように、非 nilable 型から nilable 型への変換は、暗黙的に行なわれます。
// @lnsFront: ok
let val:int! = 1; // int! <-- int
一方で nilable 型から非 nilable 型への変換は、明示的に行なう必要があります。
LuneScript では、nilable 型から非 nilable 型への変換に、次のものを用意しています。
- unwrap
- when!
- if!
- if! let
- let!
- unwrap!
unwrap
unwrap は、 nilable 型の式を、非 nilable 型へ変換します。
例えば次のように使用します。
// @lnsFront: ok
let val1:int! = 1;
let val2:int = unwrap val1;
このサンプルで val1 は int! です。その val1 を unwrap することで、 int! から int に変換しています。
なお、次のように unwrap する値が nil だった場合、 そのプログラムは実行時エラーします。
// @lnsFront: ok
let val1:int! = nil;
let val2:int = unwrap val1; // runtime error
この実行時エラーを防ぐのが unwrap default です。 unwrap default は、変換対象の値が nil だった場合の値を指定します。
次は default を使用した例です。
// @lnsFront: ok
let val1:int! = nil;
let val2:int = unwrap val1 default 0;
この例では val1 は nil となるため、 default の 0 が unwrap の評価結果となります。
default のない unwrap の使用は、 確実に nil ではないと判っている時のみに限定してください。
when!
when! は、指定の nilable 型の 変数 が nil かどうかを判定し、分岐します。
次に when! の例を示します。
// @lnsFront: ok
fn func( val:int!, val2:int! ): int {
when! val, val2 {
return val + val2;
}
else {
return 0;
}
}
print( func( 1, 2 ) ); // 3
print( func( nil, 2 ) ); // 0
print( func( 1, nil ) ); // 0
print( func( nil, nil ) ); // 0
この例では int! 型の val, val2 に対して when! で分岐しています。
- val と val2 が 非 nil の場合、
return val + val2
を実行 - val あるいは val2 が nil の場合、 return 0 を実行
when! は、指定の変数全てが非 nil の時に、最初のブロックを実行します。
このブロック内では、次の動作になります。
- 指定変数は unwrap された非 nilable の型となる。
- 指定変数は immutable となる。
when! に指定した変数のいずれかが nil だった場合、 else ブロックを実行します。 else は省略可能です。
なお、 when! に指定できるのは 変数だけ です。 メンバや式は書けません。
if!
if! は、指定の 式 が nil かどうかを判定し、分岐します。
次に if! の例を示します。
// @lnsFront: ok
fn func( val:int! ): int! {
return val;
}
fn sub( val:int! ): int {
if! func( val ) {
return _exp + 10;
}
else {
return 0;
}
}
print( sub( 1 ) ); // 11
print( sub( nil ) ); // 0
この例では func()
に対して if! で分岐しています。
func()
が 非 nil の場合、return _exp + 10;
を実行func()
が nil の場合、 return 0 を実行
if! は、指定の式が非 nil だった時に、最初のブロックを実行します。 このブロック内では、式の結果を _exp としてアクセスできます。 このとき、 _exp は 非 nilable 型 です。
if! に指定した式が nil だった場合、 else ブロックを実行します。 else は省略可能です。
ちなみに、 if! で指定した式が複数の値を返す場合、 最初の戻り値だけが対象です。 2 つ目以降の戻り値は無視します。
なお、 if! は次のようなネストは出来ません。
なぜならば、 内側の if! の _exp が、外側の if! の _exp によって、 shadowing されるためです。
// @lnsFront: skip
if! func( val ) {
if! func( val ) {
return _exp + 10;
}
else {
return 0;
}
}
これを防ぐためには、次の if! let を使用してください。
if! let
if! let は、 if! で判定する式の結果を格納する変数名を指定可能なバージョンです。
次は if! let のサンプルです。
// @lnsFront: ok
fn func( val1:int!, val2:int! ): int!, int! {
return val1, val2;
}
fn sub( val1:int!, val2:int! ): int {
if! let work1, work2 = func( val1, val2 ) {
return work1 + work2;
}
else {
return 0;
}
}
print( sub( 1, 2 ) ); // 3
print( sub( nil, 2 ) ); // 0
print( sub( 1, nil ) ); // 0
print( sub( nil, nil ) ); // 0
この例では if! let work1, work2 = func( val1, val2 )
を実行しています。
これは、 func()
の結果を work1, work2 に代入し、
全てが非 nil だった場合に最初のブロックを実行します。
このブロック内では work1, work2 にアクセスできます。
work1, work2 は、 非 nilable 型となります。
if! let で宣言した変数のスコープは、最初のブロックです。
何れかが nilable の場合、else ブロックを実行します。 else は省略可能です。
let!
let! は、nil 以外の初期値を持つ変数宣言を行ないます。
次に let! のサンプルを示します。
// @lnsFront: ok
fn func( val1:int!, val2:int! ): int!, int! {
return val1, val2;
}
fn sub( val1:int!, val2:int! ): int {
let mut work0 = 0;
let! work1, work2 = func( val1, val2 ) {
work1 = 0;
work2 = 0;
}
then {
work0 = 10;
};
return work0 + work1 + work2;
}
print( sub( 1, 2 ) ); // 3
print( sub( nil, 2 ) ); // 0
print( sub( 1, nil ) ); // 0
print( sub( nil, nil ) ); // 0
この例では、 let! work1, work2 = func( val1, val2 )
を実行しています。
- これは、
func()
の結果を初期値とする work1, work2 を宣言しています。 - work1, work2 いずれかが nil だった場合、最初のブロックを実行します。
- 全てが非 nil だった場合、then ブロックを実行します。 then は省略可能です。
let の文には ; が必要です。
上記のサンプルでは、 then ブロック終端に };
として ; を付加されています。
最初のブロックには、次のいずれかを処理しなければならない制限があります。
- let で宣言している変数全てに値を設定する。
- let を宣言しているスコープから抜ける。
上記の例では、 work1, work2 に値を設定していますが、 return でこの関数を抜けるようにしても OK です。
なお、上記制限が守られていない場合の動作は 未定義 です。
unwrap!
unwrap! は、 let! に似た制御です。異なるのは変数を宣言するのではなく、 既にある変数に対して代入する点です。
次は、 unwrap! の例です。
// @lnsFront: ok
fn test( arg:int! ) {
let mut val = 0;
unwrap! val = arg { print( 0 ); return; } then { val = val + 1; };
print( val );
}
test( 1 ); // print( 2 );
test( 2 ); // print( 3 );
test( nil ); // print( 0 );
上記例の val は、 int 型変数です。 この変数に、unwrap! を使って int! 型の arg を代入しています。
上記 unwrap! val = arg { print( 0 ); return; } then { val = val + 1; };
は、
次の処理を行ないます。
- arg が nil の場合、
{ print( 0 ); return; }
を実行する。 - arg が非 nil の場合、 arg を val に代入する。さらに then ブロックを実行する。
- then は省略可能です。
マップ型のアクセス
Map 型データの要素にアクセスした場合、 その結果は nilable となります。
たとえば次の場合、
// @lnsFront: ok
let val = { "abc": 1 };
let val2 = val.abc;
val2 は int ではなく、 int! となります。
なぜならば、 Map 型の要素が存在しない場合、 その評価結果は nil になるためです。
ちなみに、リスト、配列の要素アクセスは nilable にはなりません。
// @lnsFront: ok
let val = [ 1, 2, 3 ];
let val2 = val[ 1 ];
上の例では、 val2 は int! ではなく int になります。
なお、 val[ 4 ] にアクセスした場合の動作は 未定義 です。
リスト、配列にインデックスでアクセスする場合は、十分注意してください。
リスト、配列のインデックスアクセス結果が nilable になるようにも考えましたが、 やり過ぎな気がしたので実施していません。
まとめ
LuneScript は、次の仕様によって nil 安全を実現しています。
- nilable と非 nilable
- unwrap
次回はクラスについて説明します。