A Tour of Morfa

Deduced template parameters

Template parameter specification allows you to perform pattern matching on argument types. For example, an operation of extracting the underlying element type from an array type may be defined as an alias template as follows:

public template <T is U[]; deduce U>
alias ElementType = U;

The meaning of the clause T is U[]; deduce U is that the template may be instantiated with any type T of the shape U[] for some type U.

To test ElementType you may use the operator istype defined in the section on parameter specification:

import advanced.templates.parameter_specification;

static assert (ElementType<int[]> istype int);
static assert (ElementType<bool[][]> istype bool[]);

In the definition of ElemType, T is an explicit parameter and U is a deduced parameter. A deduced parameter is never provided during template instantiation and it has to be deduced from the arguments for explicit parameters.

Contrast the use of U in the template above ElementType with the following variant:

struct U {}

public template <T is U[]>
alias ElementType = U;

Since deduce U is missing, U is not a parameter but a name that refers to a struct type defined above.

Template refinement

It may sometimes be useful to strip all trailing []'s from the type, for example to map bool[][] to bool and int[][][] to int. You can do this easily using recursion on the level of types:

// The base case, when U is not an array type:
public template <T is U[]; deduce U>
alias BaseType = U;

// The recursive case:
public template <T is V[][]; deduce V>
alias BaseType = BaseType<V[]>;

static assert (BaseType<int[]> istype int);
static assert (BaseType<bool[][]> istype bool); 
static assert (BaseType<float[][][]> istype float);

To understand how BaseType works consider the evaluation of BaseType<float[][][]>. The type argument float[][][] matches both variants of BaseType: in the base variant, U matches the type float[][] and in the recursive variant, V matches the type float[]. When two (or more) variants match a given tuple of arguments, the most specific or refined variant is chosen. In this case, the specification V[][] is more specific than U[], since the former may be obtained from the latter by substituting U = V[] and the latter cannot be obtained by substituting any type expression for V. Thus the variant corresponding to the recursive case is chosen and, accordingly, BaseType<float[][][]> evaluates to an alias to BaseType<float[][]>, which evaluates to an alias to BaseType<float[]>, which in turn matches only the base case and evaluates to float.

At this point you may wonder how does parameter specification interact with template constraints. The rules are as follows:

  • Whenever two variants match, the one with more refined specification is selected (no specification is treated as the least refined one).
  • If no specification is more refined, a variant with the constraint is preferred over the one without. If the above two rules does not select a variant then we have an error.

These rules can be illustrated with the following declarations:

template <T>
const Description = "a type";

template <T is U[]; deduce U>
const Description = "a type of arrays";

template <T is U[]; deduce U> if (IsNumberType<U>)
const Description = "a type of arrays of numbers";

template <T is float32[]>
const Description = "the type of arrays of 32-bit floats";

static assert (Description<bool> == "a type");
static assert (Description<text[]> == "a type of arrays");
static assert (Description<int[]> == "a type of arrays of numbers");
static assert (Description<float32[]> == "the type of arrays of 32-bit floats");