Are anonymous type classes the right model at all? (replying to Re: Are fundeps the right model at all?)

George Russell ger@tzi.de
Thu, 21 Dec 2000 21:20:46 +0100


Alternatively, I wonder whether the current system of type classes is the right
model at all.

Although I prefer the Haskell system, I think it is instructive to compare it
with the Standard ML (SML) system of structures and functors.  My point is that
both Haskell and SML impose one of two possible extremes on the user, and
suffer for it.

With SML, it is as if all instances are explicitly named.  SML does not permit
user-defined overloading, and so SML is not capable of understanding 
something such as a "type class of things we can compare", and has a horrible
set of kludges to cope with implementing the equality operator.

With Haskell, on the other hand, there is no way of referring to a particular
instance when you want to.  We see a particular consequence of that here, in
that (unlike SML), it is not possible to associate an internal type with a 
given instance.  Another problem is that no-one has any control over what
instances get exported, because since instances are anonymous there is no way 
of referring to them.  Hence the current procedure is to expose everything to
the importer, which is surely a mistake.

So if you agree with me up to here, perhaps you are agreed that it is worth
while trying to find a middle way, in which we try to combine both approaches.
Well I'm not an expert language designer, and I'm doing this off the top of my
head late on Thursday evening, so please don't nitpick about syntax; I'm aware
that parsing will probably be difficult in all sorts of ways with exactly what
I'm writing, but that shouldn't be too hard to tweak.  In particular I have
followed SML in using "." to express qualification by something, even though
Haskell already used "." for something else, because I can't be bothered right
now to dig up a better symbol.

On the other hand if my whole approach is a pile of elephant dung I apologise
for wasting your time, and wish you a happy Christmas/holidays, but do try to
find a better way of combining the best of SML functors and Haskell classes.

Anyway here is my proposal.
(1) We extend type classes to allow them to introduce types.  Thus for example
    I would replace Marcin's first example by
       class Collectible e where
          type c -- or we could just omit the "type" keyword, trading clarity
                 -- for conciseness.
                 --  note also that we need a way of expressing a context for
                 -- "c", EG that it's an instance of Eq.
          empty :: c
          insert :: c -> e -> c
    As usual, you can refer to "empty" and "insert" right away, but you 
    can't refer to "c" without extra syntax.  We need a way of referring to 
    the particular instance of Collectible.  So I suggest something like:

    singleton :: (method | Collectible e) => e -> method.c
    singleton el = insert empty el
(2) We extend instance declarations in two ways.  Firstly and obviously, we 
    need a way of declaring the type c in the instance second declaration.  
    The second thing is to introduce named instance declarations, like this:

    instance IntList | Collectible Int where
       type c = [Int]
       empty = []
       insert = (flip(:)) 

    To actually _refer_ to a specific instance, you would qualify with IntList.
    So you could refer to IntList.c,  IntList.empty, IntList.insert, just like
    you would with SML.  But as with Haskell, "empty" and "insert" would
    continue to be available implicitly.

    A more complicated example arises when you have instances depending on 
    other instances.  EG

    instance SetCollection | Ord el => Collectible el where
       type c = Set el
       empty = emptySet
       insert = addToSet -- new function, thank Simon Marlow

    Then, in this case, you would refer to SetCollection.c when you wanted to
    refer to the type c.   However note that in this case we are implicitly
    using an anonymous use of Ord.  Supposing you had previously defined
    (ignoring questions about overlapping instances for now . . .)

    instance EccentricOrd | Ord Int where
       ...
    and you wanted to define Sets in terms of EccentricOrd.  Then I suggest 
    that you use instead SetCollection(EccentricOrd).c and likewise 
    SetCollection(EccentricOrd).empty and Sets(EccentricOrd).insert, 
    though I hope that such monstrous constructions will not often 
    be necessary.   When they are, maybe it would be a good idea to allow the
    user to abbreviate, as in
       instance EccentricSet | Collectible Int = SetCollection(EccentricOrd)
    just as you can do in SML.
(3) Finally it would be nice to extend the module syntax to allow named
    instances to be selectively exported and imported, just like variables.  
    If I could ignore all pre-existing Haskell code I would specify that
    whenever a module has a specific import list, no instances are imported
    unless specified.  However this is politically impossible, so instead I
    suggest that all anonymous instances continue to be implicitly imported, 
    as now, but that named instances are only imported when named in the 
    import list.  EG "import File(instance SetCollection)".  Also, I think it 
    would be nice to have something similar to the "qualified" operator, by 
    which class membership is NOT automatically inherited, and would have to 
    be explicitly specified by referring to "SetCollection.insert" or indeed 
    "SetCollection.singleton"; in particular this would provide a clean way 
    of handling overlapping classes.
OK, so I realise this is probably not the final answer, but wouldn't it be 
nice if something along these lines could be got to work?