tech

[English] [Japanese]

19. generics

Here we describe the generics that LuneScript supports.

LuneScript originally supported generics only for the built-in types List/Array/Map, but now supports generics for user-defined functions and classes as well.

function

Declare the generics for the function like this:

// @lnsFront: ok
fn func<T>( val:T ) : Set<T> {
   return (@ val);
}

As you can probably understand by looking at the above sample, when declaring a function, declare it with the function name + <formal type parameter>.

where func<T>() is a function that produces a Set<T> with the values of its arguments.

You can specify multiple formal type parameters.

// @lnsFront: ok
fn func<T1,T2>( val1:T1, val2:T2 ) : Set<T1>, List<T2> {
   return (@ val1), [ val2 ];
}

nilable, mutable

Formal type parameters have the same concept of nilable and mutable as ordinary types.

This allows you to write something like:

// @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 returns its argument in plain type T .
  • func2 returns its argument in immutable type &T.
  • func3 returns its argument as nilable type T!.

As a result, test1.insert() is possible, but test2 and test3 will result in compilation errors.

Note that nilable and mutable can be used when using formal type parameters, but cannot be used when declaring formal type parameter names. In other words, func1<T!> like this is an error:

// @lnsFront: error
fn func1<T!>( val:T ) : T {
   return val;
}

Also, nilable cannot be used for formal type parameters. In other words, it will error if:

// @lnsFront: error
fn func1<T>( val:T ) : T {
   return val;
}
let val:int! = 1;
print( func1( val ) ); // error type mismatch

In this example, func1( val ) gives an int! to func1() , but the val of func1<T>(val:T) is T , and the formal type parameter cannot be nilable, so it is an error.

If you want to do something like this, declare it like this:

// @lnsFront: ok
fn func1<T>( val:T! ) : T {
   return unwrap val;
}
let val:int! = 1;
print( func1( val ) ); // ok

In other words, fn func1<T>( val:T! ) declares that the argument of func1() is nilable.

However, not being able to handle nilable values can be inconvenient. We provide Nilable<T> for such cases.

We will discuss this separately at a later date.

sample

Using generics, you can write something like this:

// @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" ) produces a Set<str> of (@ "foo" ) and func( 1 ) produces a Set<int> of (@ 1 ) .

As you can see above, the type parameters are determined according to the arguments called.

effect

By using stem instead of generics, similar processing can be described as follows.

// @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 );
}

However, in this case, type information such as str and int will be rounded to stem . Casting @@str or @@int is necessary because it will be rounded to the stem.

Casting is inconvenient and, above all, extremely dangerous.

Generics allow safe access without casts.

class

Declare your class's Generics like this:

// @lnsFront: ok
class Test<T> {
   let val:T;
   pub fn func() : List<T> {
      return [ self.val ];
   }
}

For classes, specify the formal type parameters when specifying the name of the class declaration.

When declaring a method outside the class declaration, declaration of formal type parameters is unnecessary as follows.

// @lnsFront: skip
pub fn Test.func2() : Set<T> {
   return (@ self.val );
}

Create an instance of the Generics class like this:

// @lnsFront: skip
let test = new Test<str>( "abc" );

In addition, if all formal parameters are used for the arguments of the constructor, it is possible to omit the actual type parameters as follows.

// @lnsFront: skip
let test = new Test( "abc" );

The interface is not generics aware.

method

A method can have both class formal type parameters and method formal type parameters.

The following example is Test.func() The method has a class formal type parameter T and a method formal type parameter 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" );
}

Overriding methods with type parameters as return values

It is not possible to override a method func whose return value has a type parameter like the following.

// @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;
   }
}

Overriding such a method requires a special declaration of the type parameter of the inheriting class.

For example:

// @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;
   }
}

In this example, the Sub class inherits from Super<T> as Super<A=int>.

This Super<A=int> declares that the type parameter int is used as the A type. And specify A as the return type of the overriding func().

This allows you to override methods that have type parameters as return values.

Constraints on type parameters

An actual type parameter can be any type except nil .

For this reason, only type-independent operations such as == and ~= work on values of formal parameter types within generics classes and functions.

With this, it is not possible to write effective processing in the processing within Generics.

So we use type parameter constraints.

sample

Here is a sample type parameter constraint:

Here, the declaration class Test<T:Val> restricts the formal type parameter of the Test class to the Val class.

This allows you to call the method func() of the Val class on the value of val within the Test.sub() method.

// @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() );
   }
}

Here is an example using this Test class.

// @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

The configuration for this sample is:

  • Val1 and Val2 classes are classes that inherit Val class
  • The func() function has an argument test of type Test and calls the test.sub() method.
  • new Test( new Val1() ) and new Test( new Val2() ) generate Test type instances of the real type parameters of Val1 and Val2 and call the func() function.

As a result, Val1.func() and Val2.func() are called and this is val1 and this is val2 are output.

Note that T of Test<T:Val> must be Val, so specifying new Test<"abc">, for example, will result in an error.

Because "abc" is of type str and str is not of type Val.

By the way, the syntax of formal type parameter constraint is the same as extend of class.

So:

// @lnsFront: skip
class Hoge<T:SuperClass(IF,...)> {
}

where SuperClass is the class and IF is the interface.

SuperClass and IF are optional.

Mapping of generics class

LuneScript has a Mapping function that converts class instances to Map objects.

Regarding Mapping, please refer to the following article:

../classmapping

To map a class, the class must extend the Mapping interface.

Here is a simple example.

// @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 );
}

The Test<T> class extends Mapping. This makes the Test<T> class Mappable.

The above case is no different from normal non-Generics classes. This is because it does not have a formal parameter type member.

It is an error to have a member of a formal type parameter type as follows.

// @lnsFront: error
class Test<T> extend (Mapping) {
   let txt:T;
}

This is because, in order for a class to extend Mapping, all members of the class must be mappable, whereas the formal type parameter T, which is the type of member txt, can be any type other than nilable. because it can be

To avoid this, place a Mapping constraint on the formal type parameter that you use as the type of the member.

Specifically, it is Test<T:(Mapping)> as follows.

// @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" );
}

This makes the Generics class Mappable.