Comparing class alias proposals

From HaskellWiki
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

John Meacham's proposal for type class aliases has been around for a few years now. It has not been implemented yet; instead, several variations to his original proposal have come up. In this document we collect those ideas and their motivations in a hopefully clear overview. We also point out potential pitfalls or things that need more pondering: many ideas look very attractive and promising but prove troublesome when made formal. We believe this is the main reason they haven't been implemented yet.

For each idea we give a motivation from the point of view of a stakeholder. We identify three stakeholders:

  • the designer of a class or class hierarchy;
  • the author of class instances;
  • the programmer using the classes and instances.

Furthermore, most motivations can be grouped into two categories:

  • increasing the flexibility of the class system, useful when designing new API;
  • changing an existing API while maintaining backwards compatibility.

List of proposals

We've found the following documents that propose extensions or variations of the original proposal:

Context synonyms

The most basic idea is that of a context synonym (CS):

context BoundedEnum a = (Bounded a, Enum a)
context SatisfyA f s = (Satisfy f, Alternative f, Input f ~ s)

A context synonym may appear anywhere a context is allowed:

  • in the type signature of a function;
  • in a data type or constructor;
  • in a superclass constraint.

The arguments to a CS have to be kind-checked. In the examples above:

Bounded, Enum, BoundedEnum :: * -> Context
Satisfy, Alternative :: (* -> *) -> Context
Input :: (* -> *) -> *
SatisfyA :: (* -> *) -> * -> Context

Context synonyms are synonymous and interchangable with their right-hand sides, much like type synonyms. Specifically, they do not define any new classes (dictionaries) or functions. Ideally, they can be exported and documented, just like type synonyms.

Motivation: CS's are useful when a particularly long context keeps popping up, something that often happens when type-level programming is applied, for example with generic programming. They are interesting for programmers using already existing classes.


Higher-order context synonyms

Possible additions to the context synonyms include:

May a CS's RHS refer to other CS's? For example:

context Foo a = (BoundedEnum a, Eq a, Ord a)

This introduces the possibility of loops. Should loops be forbidden? It could be argued that in some cases of loops, the CS's transitive closure should be used, but this is only acceptable if the closure is finite. It seems more elegant to just forbid all loops.

Apart from type constructors, may a CS's variables be class constructors (things of kind ... -> Context) as well? For example:

context Bar a = a Int

Here Bar :: (* -> Context) -> Context

May CS's be passed as arguments to other CS's? For example:

context Biz = Bar BoundedEnum

May CS's be partially applied? Consider these alternate definitions of BoundedEnum:

context BoundedEnum a = (Bounded, Enum) a
context BoundedEnum = (Bounded, Enum)

This would overload the , separator to accept not just things of kind * as operands, but things of any kind, as long as the kinds of both operands are equal.

Motivation: these ideas are direct extensions of context synonyms and are useful for clients.


Instances of class aliases

One of John Meacham's main reasons for his proposal is the lack of possibilities for abstraction in the current class system: "there is no way to hide the fact that you changed a class hierarchy", he writes. Therefore, he introduces the possibility of writing instances of class aliases:

class alias Num a = (Additive a, AdditiveNegation a, Multiplicative a, FromInteger a)

instance Num a where
  zero        = ...
  (+)         = ...
  (-)         = ...
  negate      = ...
  one         = ...
  (*)         = ...
  fromInteger = ...

Motivation: Class designers can now later split up a class in smaller classes if they redefine the initial class as a class alias for the new, smaller type classes. More specifically, this could solve the Num hierarchy problem: how to redefine the Num-related type classes in the prelude while maintaining backwards compatibility?

John's example above introduces new functions zero and one which conflicts with the idea of splitting up Num in strict subclasses. However, this problem is (supposedly?) solved by overriding superclass defaults; see below. Also, signum and abs are not present in the example, but they may be added to the appropriate subclasses.

John's proposal can be decomposed into two smaller steps:

  • being able to write one instance header for several instances at the same time
  • then replacing the several type classes by a class alias.

It seems that at this point a CS is still interchangable with its RHS at all times.

Class alias or context synonym?

Can class aliases as used above be read as "context synonyms"? Certainly not without noting that context synonyms may include equality constraints, which make no sense when writing instances of class aliases. If class aliases are to piggyback on context synonyms, then we need to think carefully about what form such a context synonym should have for instances to make sense. Equality constraints are most likely not the only difference.

Overriding superclass defaults

In normal type classes

Consider:

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

In current Haskell, the header means two things:

  • if you want to write an instance of Applicative, your type should already be an instance of Functor;
  • wherever a constraint (dictionary requirement) Applicative appears, a constraint Functor is implied.

However, such a superclass constraint is often added to mean: if a type is Applicative, then we trivially have an instance of Functor (namely by defining fmap = (<*>) . pure). Sadly, there is no way to automate this in Haskell; programmers still have to define explicit instances for Functor. The following comes close:

instance Applicative f => Functor f where
  fmap = (<*>) . pure

Except this doesn't work well with the way instances are resolved now: the head of the instance basically says: any type is a Functor. The superclass constraint isn't checked until after the irrevocable match has been made.

Class system extension proposal suggests something like the following to fix this issue:

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

  fmap = (<*>) . pure

I.e. allow a class to provide or override default implementations of functions from superclasses. This means that writing an instance of Applicative implies also writing an instance of Functor. What should happen if there already is an instance of Functor in scope?

In context aliases

Adding new functions