19. generics 編
ここでは、 LuneScript がサポートする Generics について説明します。
LuneScript では当初、組込み型の List/Array/Map でのみ Generics をサポートしていましたが、 ユーザ定義の関数やクラスでも Generics をサポートしました。
関数
関数の Generics は次のように宣言します。
// @lnsFront: ok
fn func<T>( val:T ) : Set<T> {
return (@ val);
}
上記サンプルを見ればだいたい理解できると思いますが、
関数宣言する際に 関数名 + <仮型パラメータ>
で宣言します。
ここで func<T>() は、引数の値を持つ Set<T> を生成する関数です。
仮型パラメータは、複数指定することができます。
// @lnsFront: ok
fn func<T1,T2>( val1:T1, val2:T2 ) : Set<T1>, List<T2> {
return (@ val1), [ val2 ];
}
nilable, mutable
仮型パラメータは、通常の型と同じく nilable や mutable の概念を持ちます。
これにより、次のような処理を書けます。
// @lnsFront: error
fn func1<T>( val:T ) : T {
return val;
}
fn func2<T>( val:T ) : &T {
return val;
}
fn func3<T>( val:T ) : T! {
return val;
}
let mut test1 = func1( [ 1, 2 ] );
test1.insert( 1 );
let mut test2 = func2( [ 1, 2 ] );
test2.insert( 1 ); // error test2 is not mutable
let mut test3 = func3( [ 1, 2 ] );
test3.insert( 1 ); // error test3 is nilable
- func1 は、引数をそのままの型 T で返します。
- func2 は、引数を immutable 型 &T で返します。
- func3 は、引数を nilable 型 T! で返します。
これにより、 test1.insert()
は可能ですが、
test2, test3 はコンパイルエラーとなります。
なお、ここで気を付けなければならないことは、
仮型パラメータを使用する時は nilable や mutable を利用できますが、
仮型パラメータ名を宣言するときは使用できません。
つまり、次のような func1<T!>
はエラーとなります。
// @lnsFront: error
fn func1<T!>( val:T ) : T {
return val;
}
また、仮型パラメータに nilable は利用できません。 つまり、次の場合エラーします。
// @lnsFront: error
fn func1<T>( val:T ) : T {
return val;
}
let val:int! = 1;
print( func1( val ) ); // error type mismatch
この例では、 func1( val ) によって int! が func1() に与えられますが、
func1<T>(val:T)
の val は T であり、
仮型パラメータは nilable を利用できないためエラーとなります。
このような処理を行なわせたい場合は、次のように宣言します。
// @lnsFront: ok
fn func1<T>( val:T! ) : T {
return unwrap val;
}
let val:int! = 1;
print( func1( val ) ); // ok
つまり、 fn func1<T>( val:T! )
とすることで、
func1() の引数が nilable であることを宣言します。
しかし、nilable の値を扱えないと不便なこともあります。 そのような場合に備えて Nilable<T> を用意しています。
これについては、後日別途説明します。
サンプル
Generics を利用すると、次のような処理が書けます。
// @lnsFront: ok
fn func<T>( val:T ) : Set<T> {
return (@ val);
}
foreach val in func( "foo" ) {
print( val .. "bar" ); // foobar
}
foreach val in func( 1 ) {
print( val + 100 ); // 101
}
func( "foo" ) は、 (@ "foo" ) の Set<str> を生成し、 func( 1 ) は、 (@ 1 ) の Set<int> を生成します。
上記を見ると分かる通り、コールした引数に応じて型パラメータを決定しています。
効果
Generics ではなく、stem を利用することで次のように似たような処理を記載できます。
// @lnsFront: ok
fn func( val:stem ) : Set<stem> {
return (@ val);
}
foreach val in func( "foo" ) {
print( val@@str .. "bar" );
}
foreach val in func( 1 ) {
print( val@@int + 100 );
}
しかしこの場合、 str や int などの型情報が stem に丸められてしまいます。
stem に丸められてしまうので @@str
や @@int
のキャストが必要になります。
キャストするのは不便ですし、なによりも非常に危険です。
Generics を利用することで、キャストを使わずに安全にアクセスできます。
クラス
クラスの Generics は次のように宣言します。
// @lnsFront: ok
class Test<T> {
let val:T;
pub fn func() : List<T> {
return [ self.val ];
}
}
クラスの場合、クラス宣言の名前を指定する際に仮型パラメータを指定します。
なお、クラス宣言の外にメソッドを宣言する場合、 次のように仮型パラメータの宣言は不要です。
// @lnsFront: skip
pub fn Test.func2() : Set<T> {
return (@ self.val );
}
Generics クラスのインスタンスは次のように生成します。
// @lnsFront: skip
let test = new Test<str>( "abc" );
なお、コンストラクタの引数に全ての仮パラメータを使用している場合、 次のように実型パラメータを省略することも可能です。
// @lnsFront: skip
let test = new Test( "abc" );
インタフェースは generics 対応していません。
メソッド
メソッドは、クラスの仮型パラメータと、メソッドの仮型パラメータを両方持てます。
次のサンプルは Test.func()
メソッドは、
クラスの仮型パラメータ T と、メソッドの仮型パラメータ T2 を持っています。
// @lnsFront: ok
class Test<T> {
let val:T;
pub fn func<T2>(val:T2) : Map<T,T2> {
return { self.val: val };
}
}
let test = new Test( "abc");
foreach val, key in test.func( 1 ) {
print( key .. "xyz", val + 10 );
}
foreach val, key in test.func( "ABC" ) {
print( key .. "xyz", val .. "XYZ" );
}
型パラメータを戻り値に持つメソッドのオーバーライド
以下のような型パラメータを戻り値に持つメソッド func のオーバーライドはできません。
// @lnsFront: error
class Super<T> {
let val:T;
pub fn func():T {
return self.val;
}
}
class Sub extend Super<int> {
pub override fn func(): int { // error
return 1;
}
}
このようなメソッドをオーバーライドする場合、 継承するクラスの型パラメータに特別な宣言が必要です。
以下に例を挙げます。
// @lnsFront: ok
class Super<T> {
let val:T;
pub fn func():T {
return self.val;
}
}
class Sub extend Super<A=int> { // A=int
pub override fn func(): A {
return 1;
}
}
この例では、 Sub クラスは Super<T>
を Super<A=int>
として継承しています。
この Super<A=int>
は、
型パラメータ int を A 型として利用することを宣言しています。
そしてオーバーライドしている func() の戻り値の型に A を指定します。
これにより、 型パラメータを戻り値にもつメソッドをオーバーライドできます。
型パラメータの制約
実型パラメータには、 nil 以外の全ての型を指定できます。
このため、 Generics なクラスや関数内で仮型パラメータ型の値に対する処理は
==
や ~=
などの型に依存しない演算に限られます。
これだと、Generics 内の処理で効果的な処理を書けません。
そこで、型パラメータの制約を利用します。
サンプル
次は、型パラメータ制約のサンプルです。
ここでは、 class Test<T:Val>
と宣言することで、
Test クラスの仮型パラメータは Val クラスに限られます。
これにより、 Test.sub()
メソッド内で val の値に対し、
Val クラスのメソッド func()
をコールできるようになります。
// @lnsFront: ok
abstract class Val {
pub abstract fn func(): str;
}
class Test<T:Val> {
let val:T;
pub fn sub() {
print( "this is " .. self.val.func() );
}
}
次は、この Test クラスを使ったサンプルです。
// @lnsFront: ok
abstract class Val {
pub abstract fn func(): str;
}
class Test<T:Val> {
let val:T;
pub fn sub() {
print( "this is " .. self.val.func() );
}
}
class Val1 extend Val {
pub override fn func(): str {
return "val1";
}
}
class Val2 extend Val {
pub override fn func(): str {
return "val2";
}
}
fn func1( test:Test<Val1> ) {
test.sub();
}
fn func2( test:Test<Val2> ) {
test.sub();
}
func1( new Test( new Val1() ) ); // this is val1
func2( new Test( new Val2() ) ); // this is val2
このサンプルの構成は次になります。
- Val1, Val2 クラスは、Val クラスを継承したクラス
func()
関数は Test 型の引数 test を持ち、test.sub()
メソッドをコール。new Test( new Val1() )
,new Test( new Val2() )
によって、 Val1 と Val2 の実型パラメータの Test 型のインスタンスを生成しfunc()
関数をコール
これにより、 Val1.func()
, Val2.func()
がコールされ
this is val1
, this is val2
が 出力される。
なお、 Test<T:Val>
の T は Val である必要があるので、
例えば new Test<"abc">
のような指定はエラーになります。
なぜならば "abc" は str 型であり、 str 型は Val 型ではないからです。
ちなみに仮型パラメータの制約の syntax は class の extend と同じです。
つまり次のようになります。
// @lnsFront: skip
class Hoge<T:SuperClass(IF,...)> {
}
ここで SuperClass はクラスで、IF はインタフェースです。
SuperClass, IF は、それぞれ省略可能です。
generics クラスの Mapping
LuneScript は、 クラスのインスタンスを Map オブジェクトに変換する Mapping 機能を持ちます。
Mapping に関しては、次の記事を参照してください。
あるクラスを Mapping するには、 そのクラスが Mapping インタフェースを extend する必要があります。
次は簡単な例です。
// @lnsFront: ok
class Test<T> extend (Mapping) {
let txt:str;
pub fn func( val:T ) {
print( self.txt, val );
}
}
let test = new Test<int>( "hoge" );
let map = test._toMap();
if! let test2 = Test<int>._fromMap( map ) {
test2.func( 1 );
}
Test<T> クラスは Mapping を extend しています。 これにより、Test<T> クラスは Mapping 可能になります。
上記の場合は、 Generics でない通常のクラスの場合と何も変りません。 これは、仮型パラメータ型のメンバに持たないためです。
次のように仮型パラメータ型のメンバを持った場合は、エラーになります。
// @lnsFront: error
class Test<T> extend (Mapping) {
let txt:T;
}
なぜならば、あるクラスが Mapping を extend するには、 そのクラスが保持する全てのメンバが Mapping 可能でなければならないのに対し、 メンバ txt の型である仮型パラメータ T は、nilable 以外の全ての型になり得るからです。
これを回避するには、 メンバの型として使用する仮型パラメータに Mapping の制約を設定します。
具体的には次の通り Test<T:(Mapping)>
とします。
// @lnsFront: ok
class Test<T:(Mapping)> extend (Mapping) {
let val:T {pub};
}
let test = new Test( "abc" );
let map = test._toMap();
if! let test2 = Test<str>._fromMap( map ) {
print( test2.$val .. "xyz" );
}
これにより、 Generics クラスが Mapping 可能になります。