Below are my ideas for two languages extensions which I think add a lot to the Haskell language without adding too much ambiguity. At least, I haven’t found any issues with the ideas so far, but I’m sure plenty of other people will be able to
.
AutomaticClassSynonyms
Let’s say I’ve got the Failure class, and I’d like to define a MonadFailure class- simply for convenience- which is a subclass of both Failure and Monad. Well, defining the class is easy:
class (Monad m, Failure m) => MonadFailure m
I believe that this should automatically make anything which is an instance of both Monad and Failure an instance of MonadFailure, since the definition of MonadFailure is completely empty. I look at class instances as needing to address two issues:
- Existence: is there some instance which makes sense?
- Uniqueness: of those instances which make sense, which one should I use?
Here, there is no room for ambiguity: there exists an instance which makes sense (eg, instance MonadFailure Maybe), and there is precisely one instance which makes sense. There is no alternative way to define this instance.
Therefore, I think that in this case we should not complain if we have two instances for the same data type, since we know that the instances will be identical. That would make this extension work very nicely with existing code. It also adds no new syntax.
What I didn’t say
I specifically do not think this extension should make automatic instances of classes which have default definitions for all its functions. The first example that comes to mind is Exception: even though both fromException and toException have default definitions, I think the user should still have to explicitly instanciate exception, even if a type is already an instance of Typeable and Show.
SubClassOverloading
This extension is a bit more complicated. For motivation, let’s look at the interaction between Monad and Applicative. For most cases, a Monad can define an Applicative instance as such:
instance Functor MyMonad where fmap = liftM instance Applicative MyMonad where pure = return (<*>) = ap
Well, that’s irritating! Instead of just writing a five line Monad instance, I have to write five extra boilerplate lines.
As a separate issue, Applicative is not defined as a superclass of Monad, and therefore I cannot treat all Monads as Applicatives. But we can’t add that superclass requirement without breaking existing code.
So I say we allow the definition of Monad as such:
class Applicative m => Monad m where fail s :: s -> m a -- or we could just take this out... (>>=) :: m a -> (a -> m b) -> m b -- the same return :: a -> m a -- also the same fmap = liftM -- a default definition for a superclass function pure = return (<*>) = ap
And suddenly all Monads are Applicative! Since every function in the Functor and Applicative classes is a given default definition in Monad, they can be automatically derived.
But what if you want to define a special version of fmap? Simple: do it like always! The definition in Monad is merely the default; if the compiler finds a separate instance for your data type, it uses that instead. This way, old code still works without a hitch.
The downside
The only downside I can see is that suddenly you’ll have instances of classes where before there were none. Not that having Applicative instances in and of itself is a downside, but there might be cases where it would define inappropriate instances (not that I can think of any off-hand). On the other hand, this would be mitigated slightly by the requirement of the type-class author to explicitly turn on this flag.
Let the beatings begin
Well, this is my first time suggesting any changes to Haskell, so I expect to be thoroughly scolded for my perposterous, heratical notions. Even if these suggestions are lacking, however, I hope we eventually get something which allows these kinds of features in Haskell.
December 7, 2009 at 11:43 am |
AutomaticClassSynonyms:
This extension buys very little. In addition to
class (Monad m, Failure m) => MonadFailure m
you can just write (with the appropriate language extensions)
instance (Monad m, Failure m) => MonadFailure m
… and you’re done.
SubClassOverloading:
Fully-automatic instance generation is almost certainly a mistake. There are plenty of types which have a Monad instance defined in the same module, and an Applicative instance defined elsewhere; you’d break all of these, automatically, in a way the compiler maybe won’t even detect. It’d also mean you can’t read the code and figure out which instances it defines (without reference to the source code of the classes it’s defining instances of).
There’s a SuperClassDefaults proposal which resolves this issue in a not-too-verbose manner; see here:
http://www.haskell.org/haskellwiki/Superclass_defaults
That proposal, however, only allows defaults for superclasses, which means that Monad can’t trivially define Applicative defaults. One can resolve that with a ‘MonadAndApplicative’ class which defines extra superclass defaults, though…
December 7, 2009 at 11:56 am |
@lilac
AutomaticClassSynonyms:
You’re right that you can do this with the appropriate language extensions. I think this extension would be less invasive than those other language extensions, and I don’t see it costing us anything.
SubClassOverloading:
I don’t see why having the Applicative instance declared elsewhere would be a problem. I would imagine it working very similarly took overlapping instances, so that once you import the other modules, their definition of Applicative will take over. You might complain that this will change the definition of a function based on which modules are imported; I’d say this is what you getting for having orphan instances.
SuperClassDefaults might be nice, but I think my proposal is easier to follow, and introduces no new syntax. Also, keep in mind when doing comparisons: I only gave the example of Applicative as one use case; there are many other use cases that do not involve the issues you are alluding to.
December 7, 2009 at 3:07 pm |
I frequently use idioms like this in a way your proposal would break. In particular, the following definitions are useful:
class Monoid m where
(**) : m -> m -> m
class (Monoid m) => CommutativeMonoid m
The CommutativeMonoid instance should only be declared if the Monoid operation is actually commutative, and then functions that assume that the monoid operation is commutative can declare that.
December 7, 2009 at 3:28 pm |
That’s a good counter example, thank you. I’d have two options for you:
Not sure if that would be acceptable to you.
December 7, 2009 at 4:48 pm |
There is “class alias” proposal at http://repetae.net/recent/out/classalias.html which suggests something very similar, but in a way that avoids all the incompatibilities and difficulties.
December 7, 2009 at 6:34 pm |
@Jeff,
Looks like a really good proposal. You have any information on its status? As far as I can tell, it actually *does* address all the issues I’m having (plus quite a few more). Looks like the only downside would be the alias keyword and ugly interactions with other extensions- not that I can think of any.
Thanks for the link!
December 8, 2009 at 9:58 pm |
Have you looked at this pretty recent paper? It looks systematically at the problem managing constraints on types, and proposes more general solutions (I think) than the class alias proposal.
http://www.cs.kuleuven.be/~toms/Research/papers/constraint_families.pdf
December 9, 2009 at 9:17 pm |
Then there is also http://haskell.org/haskellwiki/Comparing_class_alias_proposals
which aims to collect and compare the different class alias variations.