Type parameters are, in a sense, “universally quantified”. If we have a function:
generic [T: Type]
function identity(x: T): T is
return x;
end;
We really can’t do anything with x
other than shuffle it around data
structures and return it, since we know nothing about it. We can’t write:
generic [T: Type]
function identity(x: T): T is
return x + x;
end;
Since it might not be a numeric type. Similarly, we can’t write:
generic [T: Type]
function identity(x: T): T is
print(x);
return x;
end;
Since it might not be printable, the type of the value we pass in might not
implement the Printable
typeclass.
Type constraints allow us to circumvent this. We can tell the compiler to only accept types that implement certain type classes.
For example, suppose we have:
typeclass Equatable(T: Free) is
method isEqual(a: T, b: T): Bool;
end;
Then we can write a isNotEqual
function like this:
generic [T: Free(Equatable)]
function isNotEqual(a: T, b: T): Bool is
return not isEqual(a, b);
end;
The type parameter T: Free
is modified to T: Free(Equatable)
. If we try to
call isNotEqual
with a type that doesn’t implement Equatable
, the compiler
will complain.
Multiple type classes can be specified in a comma-separated list, e.g.:
generic [T: Type(TotalEquality, TotalOrder, Printable, Serializable)]