Type class and instance declarations
Silver supports ad-hoc polymorphism via type classes. These are closely modeled on type classes in languages such as Haskell.
class Eq a {
eq :: (Boolean ::= a a) = \ x::a y::a -> !(x != y);
neq :: (Boolean ::= a a) = \ x::a y::a -> !(x == y);
}
instance Eq Integer {
eq = eqInteger;
neq = neqInteger;
}
instance Eq Float {
eq = eqFloat;
}
Eq
is a type class in the standard library, providing the functions eq
and neq
(the ==
and !=
operators forward to calls to these functions.) Implementations of these functions are provided for various types by defining instances. The Eq
type class provides defaults for both of these methods, such that instances need only provide an implementation for one function or the other.
For some type classes (currently Eq
and Ord
), the compiler can automatically generate instances for nonterminals:
derive Eq, Ord on MyNT;
Type classes may be used as constraints on variables in type signatures; any constraints on a function’s type must be resolved in order to use the function. The members of a type class automatically have the declared class as a constraint; for example in type checking eq(42, 128)
the constraint Eq Integer
must be resolved, by looking up that instance.
Function and production signatures can also introduce type constraints, allowing type class members to be used with polymorphic types:
function allEqual
Eq a => Boolean ::= xs::[a]
{
return
case xs of
| h1 :: h2 :: t -> h1 == h2 && allEqual(h2 :: t)
| _ -> true
end;
}
Instances can also have type constraints:
instance Eq a => Eq [a] {
eq = \ x::[a] y::[a] -> length(x) == length(y) && all(zipWith(eq, x, y));
neq = \ x::[a] y::[a] -> length(x) != length(y) || any(zipWith(neq, x, y));
}
instance Eq a => Eq Maybe<a> {
eq = \ x::Maybe<a> y::Maybe<a> ->
case x, y of
| just(w), just(z) -> w == z
| nothing(), nothing() -> true
| _, _ -> false
end;
}
instance Eq a, Eq b => Eq Pair<a b> {
eq = \ x::Pair<a b> y::Pair<a b> -> x.fst == y.fst && x.snd == y.snd;
neq = \ x::Pair<a b> y::Pair<a b> -> x.fst != y.fst || x.snd != y.snd;
}
To satisfy any of these instances, the constraints on the matched instance must be satisfied as well. For example eq([42], [42])
would need to satisfy Eq [Integer]
, which matches the instance Eq [a]
, in turn giving the constraint Eq Integer
that must be satisfied.
Type classes can extend existing type classes with additional members:
class Eq a => Ord a {
compare :: (Integer ::= a a) = \ x::a y::a ->
if x == y then 0 else if x <= y then -1 else 1;
lt :: (Boolean ::= a a) = \ x::a y::a -> compare(x, y) < 0;
lte :: (Boolean ::= a a) = \ x::a y::a -> compare(x, y) <= 0;
gt :: (Boolean ::= a a) = \ x::a y::a -> compare(x, y) > 0;
gte :: (Boolean ::= a a) = \ x::a y::a -> compare(x, y) >= 0;
}
instance Ord Integer {
compare = \ x::Integer y::Integer -> x - y;
lt = ltInteger;
lte = lteInteger;
gt = gtInteger;
gte = gteInteger;
}
instance Ord a => Ord [a] {
lte = \ x::[a] y::[a] ->
case x, y of
| h1::t1, h2::t2 -> h1 <= h2 && t1 <= t2
| [], _ -> true
| _, _ -> false
end;
}
Here the Ord
type class extends Eq
. Any constraint Ord a
implicitly also adds a constraint for Eq a
, and any instance of Ord
must have a corresponding instance of Eq
for the same type.
Type classes can be defined for higher-kinded types. For example, the Functor
type class has kind * -> *
:
class Functor (f :: * -> *) {
map :: (f<b> ::= (b ::= a) f<a>);
}
instance Functor [] {
map = \ f::(b ::= a) l::[a] ->
if null(l) then []
else f(head(l)) :: map(f, tail(l));
}
instance Functor Maybe {
map = \ f::(b ::= a) m::Maybe<a> ->
case m of
| just(x) -> just(f(x))
| nothing() -> nothing()
end;
}
Sometimes a general instance may match a type, but fail to match the instance’s type constraints. This can lead to rather confusing error messages, thus it is sometimes helpful to specify a more specific instance that always fails with nicer error message:
instance typeError "Decorated types do not support equality" => Eq Decorated a with i {
eq = error("type error");
}