Are anonymous type classes the right model at all? (replying to Re: Are
fundeps the right model at all?)
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?