公開技術情報

[English] [Japanese]

19.2. generics (2つの collection 型) 編

LuneScript v1.6.0 から collection 型の generics の扱いが増えました。

これは主に go へ変換するときに影響があるもので、 lua への変換を利用している場合は影響ありません。

この新しい collection 型の説明をする前に、 従来の collection 型の説明をします。

従来型の collection 型

LuneScript は 2019 年に generics をサポートしました。

一方、go が generics をサポートしたのは version 1.18 (2022年)からです。

そして、 LuneScript が go へのトランスコンパイルをサポートしたのは 2020 年で、 このころは go 1.15 の時代。

<https://twitter.com/DwarfJp/status/1317681809895780352?ref_src=twsrc>

つまり、 generics に対応していない go 1.15 で LuneScript の generics を動かしていた、ということです。

では、でどのように LuneScript の generics を実現していたか?というと、

『go の interface{} 型(現在の any 型)でデータを扱い、 アクセスする毎に interface{} からキャストして使っていた』

ということです。

具体的には、 LuneScript の collection 型には以下がありあす。

  • List<T>
  • Set<T>
  • Map<K,V>

これらは、 go で扱う場合に次の型として定義しています。

// List<T>
type LnsList struct {
	Items       []LnsAny
	lnsItemKind int
}
// Set<T>
type LnsSet struct {
	Items map[LnsAny]bool
}
// Map<K,V>
type LnsMap struct {
	Items map[LnsAny]LnsAny
}

ここで LnsAnyinterface{} です。

新しい collection 型

LuneScript v1.6.0 から、次の新しい collection 型を導入しています。

  • __List<T>
  • __Set<T>
  • __Map<K,V>

新しい collection 型は、prefix に __ を付加します。

これらは、 go で扱う場合に次の型として定義しています。

// __List<T>
type LnsList2_[T any] struct {
	Items       []T
	lnsItemKind int
}
// __Set<T>
type LnsSet2_[T comparable] struct {
	Items map[T]bool
}
// __Map<K,V>
type LnsMap2_[K comparable,V any] struct {
	Items map[K]V
}

見ての通り、新しい collection 型では、 go の generics を利用しています。

それぞれの型の特徴

ここでは、従来型と新しい型の特徴について説明します。

従来型の特徴

処理の柔軟性

従来型は any でデータを扱うので、動的に変換して処理を行ないます。 これによって、次のような事が可能です。

// @lnsFront: ok
let list:&List<str> = [ "abc" ];
let list2:&List<&stem> = list;
let list3:&List<str> = list2@@List<str>;
  • &List<str> 型のリストから &List<&stem> 型への代入が可能です。
  • &List<&stem> 型のリストから &List<str> 型へのキャストが可能です。

上記の処理が可能なのは、データを格納する際は any で扱い、 実際に格納されたデータにアクセスする際にキャストして利用しているためです。

また、この特徴によって、 LuneScript と Lua 間とで collection 型のデータのやりとりが 簡単に出来るようになっています。

例えば次のコードで obj@@Map<str,int> のキャストを行なっていますが、 このキャストが出来るのも、この特徴によるものです。

// @lnsFront: ok
fn func() {
   let code = ```
return { abc = 123 }
```;
   let obj:&stem!;
   __luago {
      if! let loaded = _load( code ## ) {
         let work = loaded(##);
         obj = expandLuavalMap( work );
      } else {
         obj = nil;
      }
   }
   when! obj {
      let map = obj@@Map<str,int>;
      foreach val, key in map {
         print( key, val );
      }
   }
}
func();

expandLuavalMap() は、 Lua から取得した値を LuneScript の値に変換する関数です。

expandLuavalMap() は、与えられた値がテーブルだった場合、 &Map<&stem,&stem> で生成して返します。

この &Map<&stem,&stem> で扱うのは不便なので、 Map<str,int> へのキャストを行なっています。

このように、 collection 型の要素の型を動的に切り替えられる、 というのは、従来型の特徴です。

オーバーヘッド

上記の通り、データを格納する際に any として格納し、 格納されたデータにアクセスする際にキャストします。

上記の処理がオーバーヘッドとなります

新しい collection 型の特徴

少ないオーバーヘッド

新しい collection 型では、 go の generics を利用します。

この go の generics により、 データアクセス時の明示的なキャストが不要になり、 オーバーヘッドが削減されています。

処理の柔軟性が無くなる

従来の collection 型の特徴で挙げた以下の処理が、 新しい collection 型では出来ません。

// @lnsFront: error
let list:&__List<str> = [ "abc" ];
let list2:&__List<&stem> = list; // error
let list3:&__List<str> = list2@@List<str>; // error

これは欠点と言えば欠点ですが、 そもそも多くの静的型付け言語の generics には、このような制限があるので、 従来型の方が例外的だった。と言えるかもしれません。

使用方法

特に意識をしない限り、従来型の collection を使っているだけで良いです。

一方で、 go へのトランスコンパイルを利用し、 なおかつ、少しでも実行パフォーマンスを改善したい場合は、 新しい collection 型を利用します。

新しい collection 型を利用するには __List などの __ prefix を付加するだけですが、 全ての型宣言に追加をするのは大変です。 そこで、デフォルトで新しい collection を利用できるようにする方法を提供しています。

以下をコードの先頭に記載することで、 そのコード内で List を宣言した場合、 __List が指定されたものとして動作します。

_lune_control default_strict_generics;

この時、従来型の collection を利用するにはどうすれば良いかというと、 _List ("_" が 1つ)を使用します。

まとめると、以下です。

default_strict_generics 宣言の有無 List _List __List
無し _List と同義 従来型 新型
有り __List と同義 従来型 新型

上記表では List 型について説明していますが、 他の Set, Map 型も同様です。

まとめ

LuneScript の collection 型で、 go の generics を利用できるように対応しました。

この機能は実験的な要素の強い機能です。

多くの場合は、従来通りの collection 型を使うだけで問題ありません。