公開技術情報

[English] [Japanese]

04. 値編

今回は、 LuneScript で扱える値について説明します。

値の型

LuneScript で扱える値の型と、トランスコンパイラ後の Lua の値との対応表を示します。

LuneScript Lua go 用途 LuneScript での定義方法
nil, null nil nil nil nil
int 数値 LnsInt 整数 0 1 2 3 ?A 0x10 -100
real 数値 LnsReal 実数 0.0 1.0 0.001
str 文字列 string 文字列, バイナリデータ "abc" 'def' ```hij```
bool 真偽値 bool 真偽値 true false
List テーブル LnsList リスト [1, 2, 3 ]
Array テーブル 配列(固定長) [@ 1, 2, 3 ]
Map テーブル LnsMap マップ { "A":1, "BC": 100 }
Set テーブル LnsSet セット (@ 1, 2, 3)
Tuple テーブル []LnsAny タプル (= 1, 2, 3)
class テーブル struct クラス class Test {}
interface テーブル interface インタフェース interface Test {}
fn function func 関数 fn func() {}
enum 数値、文字列 const enum enum Test { }
alge テーブル struct 代数データ alge Test { }
Luaval Lua の値 Lns_luaValue Lua の値そのものを
stem LnsAny nil 以外の全ての値を保持可能な型

2019/3 Set 追加。 2020/10 Luaval 追加 2023/1 タプル追加

上記の通り、 LuneScript では Lua の値を細分化してそれぞれを個別の型として扱います。

細分化の意図は、 Lua の次の仕様を改善することです。

  • Lua の数値は全て実数であり、 慣れていないと数値が実数であることによる不具合が生じる。 例えば 10/3 の Lua の計算結果は、3 ではなく 3.3333 となる。
  • Lua のテーブルは、全てのキーが自然数となる シーケンス と、 キーが自然数とならない 非シーケンス に分かれる。

    • この違いによって、テーブル内のデータを列挙する際の関数が ipairs と pairs に分かれており、使い分けが必要になる。

      • pairs だけを使っていれば問題ないとも言えるが。。。
    • テーブルのサイズを取得する # 演算子は、シーケンスのサイズを返すものであり、 非シーケンスのサイズを返さないため、紛らわしい。

nil

nil は、 Lua の nil と同じです。

LuneScript では null も利用できます。

null は nil の alias です。

null のサポートにより、 LuneScript で JSON をそのまま扱うことができます。

整数、 実数

LuneScript は、整数と実数を分けて扱います。

これにより 10/3 は 3 となり、 10/3.0 は 3.3333… となります。

型名はそれぞれ次の通りです。

// @lnsFront: ok
let val:int = 1;      // 整数 int
let val2:real = 1.5;  // 実数 real

数値リテラル

数値リテラルは C89 ライクなものを採用します。

  • 整数は 10 進数と 16 進数表現をサポート
  • 実数は 10 進数と e による指数表現。

文字

LuneScript は、 ? を使用することで ? に続く文字のコードを、 int 型の immediate な値として扱えます。

// @lnsFront: ok
print( ?a ); // 97  (0x61)

なお、 ' や " の文字のコードを得る場合、 ?\' のように \ でエスケープする必要があります。

この方法で取得可能なコードは 1 バイトだけです。 例えば ? に続く文字が UTF-8 などのマルチバイトコードだった場合、 先頭の 1 バイトを取得し 2 バイト以降は LuneScript のコードとして解析し、 parse エラーします。

四則演算

数値の四則演算は Lua と同じものを採用します。

2項演算の結果は次の通り型が変わります。

  • int と int の演算結果は int になる。
  • real と real の演算結果は real になる。
  • int と real の演算結果は real になる。

ただし、 int と int の演算結果が int の範囲外になった場合、 実行時の内部的な値としては real になりますが、LuneScript 上の型は int のままです。 演算結果を int に丸めるには、 @@int でキャストする必要があります。

go にトランスコンパイルした場合、 内部的にも int のままです。

ビット演算

ビット演算をサポートします。 Lua5.1 では使用できません。

ビット長は Lua5.2 では 32bit となります。 Lua5.3 のビット長は、環境に依存します。

  • 論理積 (&)
// @lnsFront: ok
print( 1 & 3 == 1 );
  • 論理和 (|)
// @lnsFront: ok
print( 1 | 2 == 3 );
  • 排他的論理和 (~)
// @lnsFront: ok
print( 1 ~ 3 == 2 );
  • 論理シフト(左) (|<<)
// @lnsFront: ok
print( 1 |<< 2 == 4 );
  • 論理シフト(右) (|>>)
// @lnsFront: ok
print( 0x10 |>> 2 == 4 );
  • ビット反転 (~)
// @lnsFront: ok
print( ~2 == 0xfffffffd );

文字列

" か ' で囲むと文字列になります。 "" 内では ' が使用でき、 '' 内では " が使用できます。

なお、 "", '' は改行を含めることはできません。 改行を含める場合は "\n" とします。

\n を使用せずに複数行の文字列を定義するには ``` を使用します。 `````` 内の \n は、改行ではなくそのまま \n として文字列になります。

文字列内の特定位置の文字を取得するには、 [N] を使用します。 ここで指定する N は、文字列先頭が 1 を示します。

// @lnsFront: ok
let txt = "abc";
print( txt[ 2 ] );  // 98

N が文字列長を越えた場合の動作は、 未定義 です。

文字列長は # で取得します。

// @lnsFront: ok
print( #"abc" ); // 3

型名は次の通り str です。

// @lnsFront: ok
let val:str = "abc"; // 文字列 str

連結

文字列の連結は .. で行ないます。

// @lnsFront: ok
print( "abc" .. "efg" );  // abcdefg

書式文字列

以下で書式を指定して文字列を生成できます。

// @lnsFront: ok
print( "%s %d %d" ("abc", 1, 2) ); // abc 1 2

文字列リテラルの直後に () で値を指定します。

書式などの情報は Lua の string.format API を参照してください。

真偽値(bool)

true, false をもちます。

型名は、次の通り bool です。

// @lnsFront: ok
let val:bool = true; // bool

リスト

リストは値を追加、削除可能な型です。

// @lnsFront: ok
let mut list:List<int> = [];
list.insert( 1 ); // [ 1 ]
list.insert( 2 ); // [ 1, 2 ]
list.insert( 3 ); // [ 1, 2, 3 ]
list.remove(); // [ 1, 2 ]
print( list[1] ); // 1

リストの要素には [index] でアクセスします。 リストの先頭の index は 1 です。 リストの範囲外をアクセスした場合の動作は 未定義 です。

当初の LuneScript は、 lua のトランスコンパイラとして開発を始めたため、 lua との互換性を重視し index を 1 からにしましたが、 今となってはこれは失敗だったと思っています。。

リストの長さを取得するのは # です。 例えば #list は、 リスト型の変数 list の長さを取得します。

値の追加は Lua と同じで insert(), 削除は remove() です。

型名は、次の通り List<T> です。 ここで T は、リストが保持する要素の型を示します。

// @lnsFront: ok
let val:List<int> = [1,2];

Map

Map のリテラルは JSON フォーマットを拡張したフォーマットです。

次のように JSON フォーマットを扱えます。

// @lnsFront: ok
let map = {
   "val1": 1,
   "val2": 2,
   "val3": 3
};
print( map.val1, map.val2, map.val3 ); // 1 2 3

次の点で JSON と違います。

  • キーと値に nil 以外の全ての値を使用できる
// @lnsFront: ok
let mut test:Map<int,int> = {};
let map = {
   1: "val1",
   2.0: "val2",
   test: "val3"
};
print( map[ 1 ], map[ 2.0 ], map[ test ] ); // val1 val2 val3

また、 null を nil の alias としているため、 JSON そのものを扱うことが出来ます。

// @lnsFront: ok
let mut map:Map<str,int> = {};
map[ "abc" ] = 1;
map.xyz = 10;

Map の要素には [key] でアクセスします。 key の型が str の場合、 .key としてもアクセスできます。

例えば、次の [ "abc" ] と .abc は同じ要素にアクセスするため、 次の例の print は true を出力します。

// @lnsFront: skip
print( map[ "abc" ] == map.abc ); // true

なお、マップに対して # 演算子は使用できません。

Map の型名は、次の通り Map<K,V> です。 ここで K はキーの型、V はキーに紐付ける値の型です。

// @lnsFront: ok
let val:Map<str,int> = { "abc":123 };

値の削除

前述している通り、 Map は nil の値を持てません。 これを利用し値に nil をセットすることで、 Map から削除できます。

例えば以下は、 キー "abc" に対して 123 が登録されている val に対し、 abc に nil をセットします。 これによって、 val から abc が削除されます。

// @lnsFront: ok
let val:Map<str,int> = { "abc":123 };
val.abc = nil;
let mut total = 0;
foreach _ in val {
    total = total + 1;
}
print( total ); // 1

nilable のセット

Map への nil 以外の nilable の値の代入は推奨しません。 今後、エラーとする予定です。

// @lnsFront: ok
let val:Map<str,int> = { "abc":123 };
fn func( work:int! ) {
   val.abc = work;  // warrning
}
func( 1 );

これは、Map への nilable の設定は、それが値のセットなのか、 削除なのかが不明確になってしまうためです。

なお、 immediate の nil のセット自体は今後もサポートします。

Map の注意

Map を扱う際、次を注意してください。

マップのキーは、 int と real を区別することが出来ません。

具体的には、下記の例で map[1]map[1.0] が、何を返すかは 未定義 です。

// @lnsFront: ok
let map = {
   1: "val1",
   1.0: "val2",
};
print( map[ 1 ], map[ 1.0 ] );

これは Lua 仕様由来の制限です。

なお、 go にトランスコンパイルした場合、 int と real は区別されます。

キーに int, real, str 以外を使用した場合の、キーの同値判定

次の例において、 list1, list2 はどちらも int の 1 を要素に持つリストです。 この list1 をキーとして、 "aaa" を map に登録します。

そして、 list1, list2 をキーとして、 map から値を取得すると、 その結果は aaa と nil となります。

// @lnsFront: ok
let mut map:Map<&List<int>,str> = {};
let list1 = [ 1 ];
let list2 = [ 1 ];
map[ list1 ] = "aaa";
print( map[ list1 ], map[ list2 ] );  // aaa nil

これは、 list1 と list2 が異なるキーとして判定されるためです。

int, real, str 以外を map のキーに使用した場合、 そのキーが等しいかどうかは、同じオブジェクトである必要があります。

nilable

前述の通り、Map の要素へのアクセスは次のように行なえます。

// @lnsFront: ok
let map = {
   "val1": 1,
   "val2": 2,
   "val3": 3
};
print( map.val1, map.val2, map.val3 ); // 1 2 3

ここで、map.val1 は nilable になり、 そのままでは本来のデータである int の 1 としては扱えません。

つまり、次のように map.val1 + 1 を実行することはできません。

print( map.val1 +1 )

nilable については、以下を参照してください。

../nilable

Set

値の集合を扱います。

詳しくは次の記事を参照してください。

../set/

Tuple

値の組み合わせを扱います。

詳しくは次の記事を参照してください。

../tuple/

generics

List, Array, Map は generics 対応しています。

例えば、そえぞれ次のように宣言します。

// @lnsFront: ok
let list:List<int> = [];  // int を要素に持つリスト
let array:Array<real> = [@];  // real を要素に持つ配列
let map:Map<str,int> = {}; // str をキー、int を値に持つマップ

コレクションの型

// @lnsFront: ok
let list = [ 1, 2, 3 ];
let map = { "A": 10, "B": 11, "C": 12 };

リストやマップなどのコレクションは、上記のようにリテラルを宣言できます。 この時生成される リスト、マップの型は、 構成する値によって決まります。

コレクションのコンストラクタで利用される値が全て同じ型なら、 そのコレクションの型は、その値の型となります。

例えば上記サンプルの [ 1, 2, 3 ]List<int> となります。

コレクションのコンストラクタで利用される値が異なれば、 そのコレクションの型は stem となります。

具体的には、次のようになります。

// @lnsFront: ok
let list1 = [ 1, 2, 3 ];			// List<int>
let list2 = [ 'a', 'b', 'c' ];			// List<str>
let list3 = [ 'a', 1, 'c' ];			// List<stem>
let map1 = { "A": 10, "B": 11, "C": 12 };	// Map<str,int>
let map2 = { "A": 10, "B": 11, "C": 12 };	// Map<str,int>
let map3 = { "a": 'z', "b": 'y', "c": 'x' };	// Map<str,str>
let map4 = { "a": 1, "b": 'Z' };		// Map<str,stem>

継承関係のある複数クラスを混在したコレクションのコンストラクタ

継承関係のある複数クラスを混在したコレクションのコンストラクタは、 型推論が解決できずにエラーすることがあります。

その場合は、型を明示してください。

次のサンプルを示します。

// @lnsFront: error
class Test {
}
class Sub extend Test {
}
{
   let mut val1 = [ [ new Test() ], [ new Sub() ] ]; // error
   let mut val2:List<List<Test>> = [ [ new Test() ], [ new Sub() ] ]; // ok
   let mut val3 = [ [ new Test() ], [ new Test() ] ]; // ok
   let mut val4 = [ [ new Sub() ], [ new Sub() ] ]; // ok
}

ここで、型推論を利用している val1 はエラーになります。 一方で、型を明示している val2 は OK です。 val3, val4 は、クラスを混在していないため、型推論できます。

enum

LuneScript は enum に対応しています。

詳細は次のリンク先の記事を参照してください。

../enum/

Luaval

LuneScript は Lua のコードを実行できます。 Lua コードの実行結果は、 int, real, bool, str に関しては内部的に変換を行ないますが それ以外の値は変換せずに Lua の値として処理します。

その Lua の値を保持するのが Luaval です。

詳細は以下を参照してください。

../lua/

stem

stem は、nil 以外の全ての値を保持できる型です。

LuneScript は、静的型付け言語であり、 想定する型と異なる値を与えらた場合はコンパイルエラーします。

対して stem 型は、nil 以外の全ての型を扱える型なので、 nil 以外のどのような値を与えられてもコンパイルエラーしません。

stem! は nil を含む全ての値を扱える型です。 Lua の変数そのものと考えて問題ありません。

なお、一旦 stem 型に変換すると元の型に戻すにはキャストが必要です。

キャストについては、次のリンク先を参照してください。

../cast/

form

form は関数オブジェクトを扱う型です。

関数については後述します。