A Tour of Morfa

Template constraints

The function template length from the section on function templates is a generic function: it treats all lists in an uniform way, without depending on the type of their elements. Other example of a generic function would be a function (template) that reverses a given list (code left as an exercise for you).

Sometimes you may want to define an operation applicable to a range of types, but not all of them. For example, a function that computes a sum of elements in a given list should be applicable only to a list with elements that can be added. This can be achieved using template constraints:

import advanced.templates.class_templates;
import advanced.templates.function_templates;
import morfa.type.traits: IsNumberType, IsFloatingType;

template <T> if (IsNumberType<T>)
func sum(list: List<T>): T
{
    func sum(list: List<T>, accum: T): T
    {
        return if (list == null) accum
           else sum(list.tail, accum + list.head);
    }

    return sum(list, 0);
}

In the declaration above, the clause if (IsNumberType<T>) specifies a constraint that a type T must satisfy to be a valid template argument for sum. In general, a constraint can be any Boolean expression that is evaluable at compilation time. In this case we used the template constant IsNumberType from the standard Morfa library.

unittest
{
    var someInts = cons(1, cons(2, cons(3, null)));
    assert (sum(someInts) == 1 + 2 + 3);

    var someFloats = cons(0.1, cons(0.2, cons(0.3, null)));
    assert (sum(someFloats) == 0.1 + 0.2 + 0.3);

    var someBools = cons(true, cons(false, null));
    // This gives an error about call to 'sum' not matching
    // any template declaration:
    // assert (sum(someBools) == true + false);
}

Constraints may be used with templates of any kind, not only with function templates. For example, the the const template IsNumericSubtype from section on const templates should really be defined only for argument types that have min and max properties, that is for number types:

template <T, U> if (IsNumberType<T> and IsNumberType<U>)
const IsNumericSubtypeOf = T.min >= U.min and T.max <= U.max;

Using constraints for template specialization

Constraints may be used to provide a specialized variant of a template:

template <T> if (IsNumberType<T>)
func closeTo(a: T, b: T): bool
{
    return (a - b) * (a - b) <= 1;
}

template <T>
func closeTo(a: T, b: T): bool
{
    return a == b;
}

When a call, say closeTo(1, 2), matches both variants of closeTo, compiler will choose the one with a constraint as more specific.

unittest
{
    assert (closeTo(1, 2));
    assert (not closeTo(1,3));
    assert (closeTo("morfa", "morfa"));
}

You may also provide a non-template variant for a specific type:

func closeTo(a: float, b: float): bool
{
    return (a - b) * (a - b) <= 0.000002;
}

For calls with two float arguments, the non-template variant will be preferred:

unittest
{
    assert (closeTo(0.001, 0.002));
    assert (not closeTo(0.001, 0.003));
    assert (closeTo(1, 2));
}