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
I believe the answer is a qualified yes, depending on what you're trying to achieve.
You can refrain from exporting the type class name itself from your interface module1, while still exporting the names of the type class functions. Then no one can make an instance of the class because no one can name it!
Example:
module Foo (
foo,
bar
) where
class SecretClass a where
foo :: a
bar :: a -> a -> a
instance SecretClass Int where
foo = 3
bar = (+)
The downside is no one can write a type with your class as a constraint either. This doesn't entirely prevent people from writing functions that would have such a type, because the compiler will still be able to infer the type. But it would be very annoying.
You can mitigate the downside by providing another empty type class, with your "closed" class as a super-class. You make every instance of your original class also an instance of the sub class, and you export the sub class (along with all of the type class functions), but not the super class. (For clarity you should probably use the "public" class rather than the "secret" one in all of the types you expose, but I believe it works either way).
Example:
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
module Foo (
PublicClass,
foo,
bar
) where
class SecretClass a where
foo :: a
bar :: a -> a -> a
class SecretClass a => PublicClass a
instance SecretClass Int where
foo = 3
bar = (+)
instance SecretClass a => PublicClass a
You can do without the extensions if you're willing to manually declare an instance of PublicClass
for each instance of SecretClass
.
Now client code can use PublicClass
to write type class constraints, but every instance of PublicClass
requires an instance of SecretClass
for the same type, and with no way to declare a new instance of SecretClass
no one can make any more types instances of PublicClass
2.
What all of this doesn't get you is the ability for the compiler to treat the class as "closed". It will still complain about ambiguous type variables that could be resolved by picking the only visible instance of "closed".
1 Pure opinion: it's usually a good idea to have a separate internal module with a scary name which exports everything so that you can get at it for testing/debugging, with an interface module that imports the internal module and only exports the things you want to export.
2 I guess with extensions someone could declare a new overlapping instance. E.g. if you've provided an instance for [a]
, someone could declare an new instance of PublicClass
for [Int]
, which would piggyback on the instance of SecretClass
for [a]
. But given that PublicClass
has no functions and they can't write an instance of SecretClass
I can't see that much could be done with that.