Functor hierarchy proposal and class system extension proposal

Chung-chieh Shan ccshan at post.harvard.edu
Fri Jan 7 08:28:06 CET 2011


Conor McBride <conor at strictlypositive.org> wrote in article <444CCA2D-3472-4A59-8A9F-548FADE84AF0 at strictlypositive.org> in gmane.comp.lang.haskell.prime:
> My plan also has the advantage of cheaper backward compatibility
> (for this and other (future) class splittings).

I find compatibility with future class splittings to be a compelling
feature to strive for, even *without* any default member definitions.
Inserting a new class in a hierarchy (as the Numeric Prelude proposes)
shouldn't cause any existing instances to break.  For example, say we
split

    class Num a where -- (+), (-), negate, (*), abs, signum

(I'll ignore fromInteger and the superclasses Eq and Show in this
message) into

    class Additive a where -- (+), (-), negate
    class (Additive a) => Num a where -- (*), abs, signum

In today's Haskell, all existing instances of Num will break.  The
problem is that the header "instance Num X where" used to mean "below
are definitions for (+), (-), negate, (*), abs, signum" but now means
"below are definitions for (*), abs, signum".

Here's a crazy idea to future-proof against such a split: let's make
every instance header declare not only the instance being defined but
also the superclass instance(s) being presupposed.  We will write

    instance () -> Num X where ...

to mean "below are definitions for (+), (-), negate, (*), abs, signum",
both before and after the split.  To say "below are definitions for (*),
abs, signum" (which only makes sense after the split), we will write

    instance (Additive X) -> Num X where ...

(I just made up the -> notation, and I'm not satisfied with it, though
it's definitely not the same as "instance (Additive X) => Num X where",
which needs UndecidableInstances.  Maybe "class (Additive a) => Num a"
should become "class (Additive a) -> Num a".)

Suppose that at t=3 we insert another class into the hierarchy:

    class Additive a where -- (+), (-), negate
    class (Additive a) => Ring a where -- (*)
    class (Ring a) => Num a where -- abs, signum

Now "instance (Additive X) -> Num X" still means "below are definitions
for (*), abs, signum".  To define just (*), we will write "instance
(Additive X) -> Ring X".  To define just abs and signum, we will write
"instance (Ring X) -> Num X".  We can also write "instance () -> Ring X"
to define (+), (-), negate, (*).

So each instance definition in the new syntax can explode into a whole
bunch of instance definitions in the old syntax.  I have high hopes that
this would work well with type-class synonyms.  For example, at t=4,
let's insert Monoid and Semiring into our class hierarchy:

    class Monoid a where -- (+)
    class (Monoid a) => Additive a where -- (-), negate
    class (Monoid a) => Semiring a where -- (*)
    class alias Ring a = (Monoid a, Semiring a)
    class (Ring a) => Num a where -- abs, signum

Still "instance (Additive X) -> Num X" means to define (*), abs, signum.
Still "instance (Additive X) -> Ring X" means to define just (*).
In fact even "instance () -> (Eq X, Show Y)" could be allowed.

If the same superclass appears multiple times then member names will
clash:

    class Su a where ...
    class (Su a, Su b) => Cl a b where ...

    instance (Su a) -> Cl a b where -- provide (Su b) and Cl a b
    instance (Su b) -> Cl a b where -- provide (Su a) and Cl a b
    instance (Su a, Su b) -> Cl a b where -- provide just Cl a b

    instance () -> Cl a b where
        -- not a backward compatibility concern because we couldn't have
        -- gotten our current Cl by splitting out both copies of Su, so
        -- we don't have to allow this instance at all

All this can be emulated using class aliases as proposed at
http://repetae.net/recent/out/classalias.html : as the EqOrd example
there shows, instead of the code at t=3 above, one would write

    class Additive a where -- (+), (-), negate
    class (Additive a) => Additive_Ring a where -- (*)
    class (Additive_Ring a) => Ring_Num a where -- abs, signum

    class alias Ring a = (Additive a, Additive_Ring a)
    class alias Num a = (Additive a, Additive_Ring a, Ring_Num a)
    class alias Additive_Num a = Additive a => (Additive_Ring a, Ring_Num a)

then write "instance Additive_Num X" instead of "instance (Additive X)
-> Num X".  But this requires a quadratic number of class aliases.
Besides, I like to understand *every* instance as covering not a single
edge in the hierarchy but a subgraph.

So far I haven't said anything about default member definitions.
What I want to suggest is, what if

    instance (Functor Blah) -> Applicative Blah where

means to hide the default instance for Functor Blah inside the
Applicative class, and

    instance () -> Applicative Blah where

means to use that default instance (which would still allow overriding
parts of it)?

I guess in terms of the class-alias proposal, my suggestion would be to
allow instances as part of a class alias!  I imagine a syntax like:

    class Functor a where ...
    class (Functor a) => Functor_Applicative a where ...

    class alias Applicative a = (Functor a, Functor_Applicative a) where
        fmap f x = pure f <*> x

-- 
Edit this signature at http://www.digitas.harvard.edu/cgi-bin/ken/sig
<INSERT PARTISAN STATEMENT HERE>




More information about the Haskell-prime mailing list