Is it possible to create a typeclass that can no longer admit new members (perhaps by using module boundaries)? I can refuse to export a function necessary for a complete instan
You can encode closed type classes via closed type families, which can be essentially encoded as associated type families in turn. The key to this solution is that instances of an associated type family have be inside a type class instance, and there can only be one type class instance for each monomorphic type.
Note that this approach is independent of the module system. Instead of relying on module boundaries, we provide an explicit list of which instances are legal. This means, on the one hand, that the legal instances can be spread over multiple modules or even packages, and on the other hand, that we cannot provide illegal instances even in the same module.
For this answer, I assume that we want to close the following class so that it can only be instantiated for the type Int
and Integer
, but not for other types:
-- not yet closed
class Example a where
method :: a -> a
First, we need a little framework for encoding closed type families as associated type families.
{-# LANGUAGE TypeFamilies, EmptyDataDecls #-}
class Closed c where
type Instance c a
The parameter c
stands for the name of the type family and the parameter a
is the index of the type family. The family instance of c
for a
is encoded as Instance c a
. Since c
is a class parameter as well, all family instances of c
have to be given together, in a single class instance declaration.
Now, we use this framework to define a closed type family MemberOfExample
to encode that Int
and Integer
are Ok
, and all other types are not.
data MemberOfExample
data Ok
instance Closed MemberOfExample where
type Instance MemberOfExample Int = Ok
type Instance MemberOfExample Integer = Ok
Finally, we use this closed type family in a superclass contraint of our Example
.
class Instance MemberOfExample a ~ Ok => Example a where
method :: a -> a
We can define the valid instances for Int
and Integer
as usual.
instance Example Int where
method x = x + 1
instance Example Integer where
method x = x + 1
But we cannot define invalid instances for other types than Int
and Integer
.
-- GHC error: Couldn't match type `Instance MemberOfExample Float' with `Ok'
instance Example Float where
method x = x + 1
And we cannot extend the set of valid types, either.
-- GHC error: Duplicate instance declarations
instance Closed MemberOfExample where
type Instance MemberOfExample Float = Ok
-- GHC error: Associated type `Instance' must be inside a class instance
type instance Instance MemberOfExample Float = Ok
Unfortunately, we can write the following bogus instance:
-- Unfortunately accepted
instance Instance MemberOfExample Float ~ Ok => Example Float where
method x = x + 1
But since we will never be able to discharge the equality constraint, I don't think we can ever use it for anything. For example, the following is rejected:
-- Couldn't match type `Instance MemberOfExample Float' with `Ok'
test = method (pi :: Float)