Template specialization

We have seen in the last chapter, the declaration of template symbol, that can be instanciated using any types. Sometimes, it can be usefull to restrict the set of types that can be used in a given template symbol (e.g. only slices but of any type, only classes, only structures, etc.). In order to make this possible, the Ymir language offers some elements to filter the different types that can be used.

The following code block present the complete syntax of a template parameter.

template_parameter := Identifier | of_filter | class_filter | struct_filter | impl_filter | variadic_filter

of_filter := Identifier 'of' type
class_filter := 'class' Identifier
struct_filter := 'struct' Identifier
impl_filter := Identifier impl type
variadic_filter := Identifier '...'


One can note that every template parameters have an Identifier. This identifier are the root of the specialization tree. For example, in the following template parameters {T of [U], U} there are two roots, T and U. The root U is important, these template parameters are different to {T of [U]}. In the first case, the identifier U refers to a template type (as it is a root of the parameters), and in the second case it refers directly to a type that is named U, and that has to be declared somewhere.

Of filter

The of filter is declared using the keyword of. This filter is used to specify the form of the type that can be used to instanciate the template. The form can be any form of type, it can be for example a slice, an array, a template symbol, etc. In the following example, the first function foo declared at line 6, is a template function that can be instanciated using only T=i32. The second function foo at line 14 use the of filter to inform that the type T must have the same form as the type Z, but does not filter the type Z. The third function foo at line 21, accepts for the type T any slice that have as internal type a template type declared as Z, there is no filter on Z.

import std::io;

/**
 * This function will only be callable, with a i32 as T
 */
def foo {T of i32} (_ : T) {
    println ("First ??");
}

/**
 * This function will only be callable, with a type that fit the Z pattern
 * That is to say every type
 */
def foo {T of Z, Z} (_ : T) {
    println ("Second ?");
}

/**
 * This function will be only callable, with a slice of Z, where Z can be anything
 */
def foo {T of [Z], Z} (_ : T) {
    println ("Third !");
}

def main () {
    foo ([1, 2]);
}


In the above example, one can note that, two functions can be called by the expression foo ([1, 2]). The second and third ones. The template definition that match the best the types, will be used. Here, this is the third one, the filter is more specific and thus has a better score.

Results:

Third !


The of filter is a kind of destructuring pattern. They can be chained, and composed with other filters. Here some other example, where this time the of filter is used to get the template types parameters of a given class type, and apply some filter on them.

import std::io;

/** A template class, that takes any type as template parameters */
class X {T} {
    let _x : T;
    
    pub self (x : T) with _x = x {}
}

/**
 * This function accepts any X object, as long as its template parameter is a slice
 */
def foo {T of &(X!{U}), U of [Z], Z} (x : T) {
    println ("Slice X : ", x::typeinfo.name);
}

/**
 * Accept all the X objects, that have not been accepted by the first function
 * Indeed, the template is less specific, and is used only if the first one fails
 */
def foo {T of &(X!{U}), U} (x : T) {
    println ("Not a slice X : ", x::typeinfo.name);
}


def main () {
    let a = X::new ([1, 2, 3]);
    let b = X::new ("Test");
    let c = X::new (23.0f);
    
    foo (a);
    foo (b);
    foo (c);
}


Results:

Slice X : main::X([i32])::X
Slice X : main::X([c32])::X
Not a slice X : main::X(f32)::X


As presented in the introduction of this chapter, there is a difference between identifier that can be found in the roots of the template parameters and those which are not. In the following example, the foo function accept a slice of U where U is not defined, resulting in an error by the compiler. Indeed, the type U could have been defined somewhere, and there must be a distinction between this type and a template parameter.

import std::io;

def foo {T of [U]} (a : T) {}

def main () {
    foo ([1, 2]);
}


Errors:

Error : the call operator is not defined for foo {T of [U]}(a : T)-> void and {mut [mut i32]}
 --> main.yr:(6,6)
 6  ┃ 	foo ([1, 2]);
    ╋ 	    ^      ^
    ┃ Note : candidate foo --> main.yr:(3,5) : foo {T of [U]}(a : T)-> void
    ┃     ┃ Error : undefined type U
    ┃     ┃  --> main.yr:(3,16)
    ┃     ┃  3  ┃ def foo {T of [U]} (a : T) {}
    ┃     ┃     ╋                ^
    ┃     ┗━━━━━┻━ 
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.

Struct and Class filters

The keywords struct and class defines respecitvely the struct and class filters. They filter the accepted types of a template symbol. Unlike of filter they cannot be chained, and accept only an identifier. In the following example, the first foo function at line 11 is instanciable using a class type, and the second at line 15 only accept struct types. Warning class filter accepts a reference class type, and not directly the class type (&A not A).

import std::io

struct 
| x : i32
-> X;

class A {
    self () {}
}

def foo {class T} () {
    println ("Class !");
}

def foo {struct T} () {
    println ("Struct !");
}

def main () {
    foo!(&A) ();
    foo!(X) ();
} 


Results:

Class !
Struct !


Even if this filters cannot be chained, they can be used as the leaf of a of filter. In the following example, the function foo accepts a slice of class objects.

import std::io;

class A {
    pub self () {}
    impl Streamable;
}

class B {
    pub self () {}
    impl Streamable;
}

def foo {T of [U], class U} (a : T) {
    println (T::typeid, " = ", a, "");
}

def main () {
    foo ([A::new (), A::new ()]);
    foo ([B::new ()]);
}


Results:

[&(main::A)] = [main::A(), main::A()]
[&(main::B)] = [main::B()]

Implement filter

We have seen in the chapter about traits (cf. Traits), that class type can implement a given trait. Implementing a trait gives specific method to a class type, that can be called. However traits lose most of their interest if it is impossible to accept a type that implements the trait without knowning the type itself. This cannot be done by inheritance, as traits are not types, however templates have a specific filter to perform this operation. In the following example, the impl filter is used by the foo function, that thus accepts any kind of object as long as they impl the trait Getter.

import std::io;

trait Getter {
    pub def get (self)-> i32;
}

class A {

    pub self () {}
    
    impl Getter {
        pub over get (self)-> i32 {
            12
        }
    }

}

def foo {T impl Getter} (a : T) -> i32 {
    a.get ()
}

def main () {
    let a = A::new ();
    println (foo (a));
}


A trait can be a template symbol. In that case it has some template parameters, that can be destructured by a template filter. For example, in the following source code, the trait Getter is a template, that is implemented using the type i32 by the class A. The first foo function at line 1 accepts any kind of object as long as they impl the trait Getter, and filter the template parameter of this trait to get it under the identifier X. The second foo function only accepts types that implements the trait but using a slice. Because the second foo function is more specific when using &B object, it is called at line 44.

import std::io;

trait Getter {T} {
    pub def get (self)-> T;
}

class A {

    pub self () {}
    
    impl Getter!{i32} {
        pub over get (self)-> i32 {
            12
        }
    }
}

class B {

    pub self () {}
    
    impl Getter!{[i32]} {
        pub over get (self)-> [i32] {
            [12, 24]
        }
    }
}

def foo {T impl Getter!{X}, X} (a : T) -> X {
    print ("First : ");
    a.get ()
}

def foo {T impl Getter!{X}, X of [U], U} (a : T)-> X {
    print ("Second : ");
    a.get ()
}

def main () {
    let a = A::new ();
    let b = B::new ();
    
    println (foo (a));
    println (foo (b));
}


Results:

First : 12
Second : [12, 24]


Variadic templates

Variadic templates are special templates, that takes an arbitrary number of type as arguments. They are defined using an identifier followed by the token .... When the specialization is done, the identifier of the variadic template, can be used to define a tuple type. In the following example, the type of the parameter a of the foo function is (i32, i32, i32, i32, i32). Warning If only one type is given to the variadic template, then it is not a tuple, but directly the type that has been given. As you may have guessed by now, the println function is a variadic template function.

import std::io

def foo {T ...} (a : T) {
    println (a.0, expand a);
}

def main () {
    foo (1, 2, 3, 4, 5);
}


Results:

112345


The identifier can also be used to complete another type. For example, a function pointer type. In the following source code, the structure X accepts an arbitrary number of type as template parameters, and use them to form the type of the field foo. When instanciated by the main function at line 12, the field foo takes the type fn (i32, f32)-> void.

import std::io

struct 
| foo : fn (T)-> void
 -> X {T...};
 
def foo (x : i32, y : f32)-> void {
    println ("(", x, ", ", y, ")");
}
 
def main () {
    let x = X!{i32, f32} (&foo);
    x.foo (12, 3.14f);
}


Results:

(12, 3.140000)


To force the type to be a tuple inside another type, the standard syntax of tuple can be used. For example, the field foo could have been defined as follows fn ((T,))-> void, in that case it would have been equal to fn ((i32, f32))-> void, meaning a function pointer that takes a parameter of type (i32, f32), and returns nothing.

Recursive variadic template

Variadic template must contain at least one type. To perfom recursive variadic function, end case functions must be written, this end case generally contains a standard template parameter. For example, the following example presents a foo function that takes variadic parameters, and prints them. The end case is described at line 3, where the function takes only one standard template parameter. The function foo at line 8 takes two template parameter, a standard one that will be used for the first parameter of the function, and a variadic one for the rest of the parameters. One can note from the line 5 that even if the type of b is c8 and thus not a tuple, the keyword expand is usable, and does nothing particular.

import std::io;

def foo {F, R...} (a : F, b : R) {
    println ("FST : ", F::typeid, "(", a, ")");
    foo (expand b);
}

def foo {F} (a : F) {
    println ("SCD : ", F::typeid, "(", a, ")");
}

def main () {
    foo (1, 3.f, "Test", 'r'c8);
}


Results:

FST : i32(1)
FST : f32(3.000000)
FST : [c32](Test)
SCD : c8(r)