RE: [Haskell] PROPOSAL: class aliases
| This is a proposal for a language extension which will hopefully mitigate the | issues holding back evolution of the standard prelude as well as provide | useful class abstraction capabilities in general. A short summary would be "type synonyms for class constraints". You'd definitely want the syntax to look as much like a type synonym decl as possible. I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between (i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a) Note that (i) is Haskell 98. * In both cases one can write f :: (CD a) => ... instead of the more voluminous f :: (C a, D a) * However with (i), for each type T one must write instance C T where { ...meths for C... } instance D T where { ...meths for D... } instance CD T where {} whereas with (ii) one can write instance CD T where { ...meths for C... ...meths for D... } I believe that this latter is the sole difference. Am I right? [Implementation aspects aside.... with (i) GHC will pass one dictionary CD containing a pair of dictionaries, one for C and one for D.] If so, than rather than invent a whole new mechanism, why not simply extend the existing superclass mechanism to allow a single instance decl to declare instances for several classes? For example, one add to Haskell 98 the following: an instance declaration for a class CD with superclasses C and D may give the instances for its superclasses C and D [One could quibble about details. E.g Should the class decl for CD *say* whether the instance decl *must* contain decls for the superclass methods? Or can one vary it on a instance-by-instance basis, which might be more flexible?] Anyway, my main point it: would a smaller change not suffice? Simon
On Thursday 13 October 2005 13:21, Simon Peyton-Jones wrote:
If so, than rather than invent a whole new mechanism, why not simply extend the existing superclass mechanism to allow a single instance decl to declare instances for several classes? For example, one add to Haskell 98 the following: an instance declaration for a class CD with superclasses C and D may give the instances for its superclasses C and D
[One could quibble about details. E.g Should the class decl for CD *say* whether the instance decl *must* contain decls for the superclass methods? Or can one vary it on a instance-by-instance basis, which might be more flexible?]
I just want to mention Robert Will's proposal for "delayed method definitions"; see http://www.stud.tu-ilmenau.de/~robertw/dessy/fun/ sections 4.3.1 and 4.3.2, which is quite similar to yours. Cheers, Ben
I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between
(i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a)
The difference is that (i) is, in a sense, generative - because you still have to declare a type to be an instance of CD even if it is one of both C and D. That is not only inconvenient it can even create problems, for modular program development, as class instances always cross module boundaries [which is a wart]. So if there are two different modules in your project needing a type to be an instance of CD, you need to find a single place where to put that instance declaration. Frankly, as long as there is no way to limit the scope of an instance it would probably even make sense to treat method-less classes as class synonyms anyway, i.e. ones that do not require instances. Though ways of limiting the scope of class instances is probably a change quite a few people would like to see, so such a change would inhibit a move in that direction in the future. Stefan Kahrs
On Thu, Oct 13, 2005 at 12:21:41PM +0100, Simon Peyton-Jones wrote:
| This is a proposal for a language extension which will hopefully mitigate the | issues holding back evolution of the standard prelude as well as provide | useful class abstraction capabilities in general.
A short summary would be "type synonyms for class constraints". You'd definitely want the syntax to look as much like a type synonym decl as possible.
I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between
Actually I think it is pretty orthogonal to superclasses, class aliases are about composing classes, not building hierarchies.
(i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a)
Note that (i) is Haskell 98.
* In both cases one can write f :: (CD a) => ... instead of the more voluminous f :: (C a, D a)
* However with (i), for each type T one must write instance C T where { ...meths for C... } instance D T where { ...meths for D... } instance CD T where {}
whereas with (ii) one can write instance CD T where { ...meths for C... ...meths for D... }
I believe that this latter is the sole difference. Am I right?
No, there are a number of differences that allow class aliases to be used for true class abstraction rather than just a shortcut to writing instances.
If so, than rather than invent a whole new mechanism, why not simply extend the existing superclass mechanism to allow a single instance decl to declare instances for several classes? For example, one add to Haskell 98 the following: an instance declaration for a class CD with superclasses C and D may give the instances for its superclasses C and D
this does not actually solve the problems mentioned in the proposal. in particular, (CD a) => a and (C a,D a) => a are distinct types. this means that you cannot use the aliases as abreviations or alternate names, which is a very nice side effect. with fine grained class hierarchies, type signatures get big fast. having a shorthand is very nice. but worse, it ruins the symmetry. declaring an instance for CD a will create instances for (C a,D a) but declaring instances for C a and D a will not create one for CD. A key point of my design is that you can declare instances in the new Num hierarchy, or in the haskell 98 one, and the instances will be propagated both directions. things get much more complicated when you realize that you might want more than just 2 views of the same hierarchy and there is not a clear order among them. if you constantly have to remember to declare instances for the old haskell 98 classes too then there is really no benefit. Another illustrative example is one that combines aliases with superclasses. class alias Num a = Show a => (Additive a, Multiplicative a) now, Show is a superclass, but Num is an alias for Additive and Multiplicative. if we declare something an instance of Num, we are declaring instances for precisely Additive and Multiplicative. but not Show, there must already be an existing instance for Show since it is a superclass and not part of the alias, if this distinction were not made then several bad things happen: it is obvious you cannot emulate the old haskell 98 behavior and thus cannot get true abstraction. declaring an instance for Num where you left out 'show' would rather than give an error as it should, use the default method for show (which is undefined). this is definitly what you don't want for Show, but it might be what you want for an alternate class with a useful default. with the superclass method you mentioned, how do we control exactly which classes we are creating instances for? all the way up the hierarchy back to the base? just one level? neither rules gives us what we want and if we put that explicitly in the instance declaration we ruin the whole point of class abstraction. (plus, it seems like the wrong place to put it anyway). An instance for a class alias always and exactly declares instances for each of its components and nothing else and is orthogonal to the superclass hierarchy. Another key way in which it is different is that it is truely a composition of classes rather than a ordering on them. with a superclass relationship, classes are forced to build on top of one another, you cannot have mutual recursion between class default methods.. for instance, consider this useful little alias: class alias EqOrd a = (Eq a, Ord a) where a == b = compare a b == EQ now you can declare something as an EqOrd and just provide a 'compare' method and it will derive everything else including the Eq methods. notice that the default method is declared the wrong way in the class hierachy. this is a very handy thing, but is actually necessary to create the abstraction benefits we want. if we look at the Num example from my previous proposal:
class (Addititive a, AdditiveNegation a, Multiplicative a, FromInteger a) => Num a where one = fromInteger 1 zero = fromInteger 0 negate x = zero - x
notice that one and zero are given definitons in terms of fromInteger, if these defaulting methods could not be done, then a standard haskell 98 instance for Num could not create proper instances for Additive and Multiplicative. we would basically be forced to only extend the class hierarchy by creating subsets of the current hierarchy, we could not add functions at the 'base' and expect them to get defined properly. Also, it should be noted that while I am using the Num hierarchy as an example, I think this is much more generally useful than just rewriting the prelude. the Lattice example I gave is right out of my toolbox and my anoyances with it are part of what motivated me to write this.
Anyway, my main point it: would a smaller change not suffice?
I do not think it suffices. We could extend the supertyping relationship some more to make it suitable, but I think we will end up with the exact same proposal but with different terminology :) John -- John Meacham - ⑆repetae.net⑆john⑈
On Thu, Oct 13, 2005 at 05:51:36AM -0700, John Meacham wrote:
On Thu, Oct 13, 2005 at 12:21:41PM +0100, Simon Peyton-Jones wrote:
Anyway, my main point is: would a smaller change not suffice?
I do not think it suffices.
We could extend the supertyping relationship some more to make it suitable, but I think we will end up with the exact same proposal but with different terminology :)
For concreteness, here's a slight narrowing of Simon's version. Given your H98 classes class Additive a where (+) :: a -> a -> a zero :: a class Additive a => Negative where (-) :: a -> a -> a negate :: a -> a x - y = x + negate y negate x = zero - x class Multiplicative a where (*) :: a -> a -> a one :: a extend the class syntax with an annotation on the assumptions (! for now), to allow class (Show a, !Additive a, !Negative a, !Multiplicative a) => Num a where fromInteger :: Integer -> a one = fromInteger 1 zero = fromInteger 0 (This is for illustration -- I'm not claiming this is the ideal factoring of the Num class.) The ! annotations would be ignored during type inference. Their only meaning is (a) the class declaration for Num may include defaults for the methods of the !'d superclasses, (b) an instance declaration for Num also defines instances for the !'d superclasses, and thus may include definitions for the methods of Num and those superclasses. Any methods of these classes not defined in the instance are assigned default definitions, with defaults in the Num class overriding any in the superclasses. Thus if a Num instance is given, a Show instance must also be in scope (as now), but Additive, Negative and Multiplicative instances cannot be given, e.g.: instance Show Int65536 where showsPrec n = showsPrec n . toInteger instance Num Int65536 where (+) = primPlusInt65536 (-) = primMinusInt65536 (*) = primMultInt65536 fromInteger = primFromInteger65536 In comparision with the class alias proposal, this loses aliasing, but retains the ability to define defaults for superclasses, which is what I've been missing for ages. All these proposals need to address repeated inheritance, as in an example from Davis Menendez: class (!Functor m) => Monad m where { fmap = liftM; ... } class (!Functor d) => Comonad d where { fmap = liftD; ... } With the above rules, it would be illegal to define instances of both these classes for the same type, but one could define class (!Monad f, !Comonad f) => MonadComonad f where ... as long as either the class includes a default definition of fmap, or the instance includes a definition: instance MonadComonad Id where fmap f (Id x) = Id (f x) ... MPTCs raise extra issues, like class (!Functor f, !Functor g) => Something f g where fmap = ... Which Functor is being given a default fmap? I'd prefer to avoid this by requiring that the !'d assumptions have exactly the same arguments as the class being defined.
Am Donnerstag, 27. Oktober 2005 13:18 schrieb Ross Paterson:
[...]
extend the class syntax with an annotation on the assumptions (! for now), to allow
class (Show a, !Additive a, !Negative a, !Multiplicative a) => Num a where fromInteger :: Integer -> a
one = fromInteger 1 zero = fromInteger 0
I don't think that such a syntax is a good idea, since class contexts are used elsewhere where such a syntax doesn't make sense.
[...]
Best wishes, Wolfgang
Simon Peyton-Jones wrote:
I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between
(i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a)
Note that (i) is Haskell 98.
I was about to suggest almost exactly the same. In particular, John's proposal could be decomposed into three parts: 1. Allow instance declarations to define methods of superclasses. These are simply converted into the appropriate instance declarations for the superclasses. 2. Allow class declarations to give defaults for methods in superclasses. Together with (1) they are used in the obvious way. 3. Allow empty instance declarations to be implicitly generated. As a nice side effect, (1) and (2) together would allow us to cleanly get rid of the fmap/liftM annoyance: *> class Functor f where { fmap :: ... } *> class Functor m => Monad m where { fmap = liftM } I'm not sure about (3). I think, to effectively make Foo a synonym for Bar, we'd need: *> class Foo a => Bar a *> instance Foo a => Bar a If the instance for every type were allowed, Foo and Bar would be indistinguishable from true synonyms. Further, if classes with no methods have no use currently, this "universal instance" could be compiler generated whenever a class without methods is declared. Or the empty class may be treated as a synonym, if that's simpler. Does this make any sense? Udo.
Udo Stenzel writes:
Simon Peyton-Jones wrote:
I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between
(i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a)
Note that (i) is Haskell 98.
I was about to suggest almost exactly the same. In particular, John's proposal could be decomposed into three parts:
1. Allow instance declarations to define methods of superclasses. These are simply converted into the appropriate instance declarations for the superclasses.
2. Allow class declarations to give defaults for methods in superclasses. Together with (1) they are used in the obvious way.
3. Allow empty instance declarations to be implicitly generated.
As a nice side effect, (1) and (2) together would allow us to cleanly get rid of the fmap/liftM annoyance:
*> class Functor f where { fmap :: ... } *> class Functor m => Monad m where { fmap = liftM }
This can also get us into trouble. Consider, class Functor f where fmap :: ... class Functor m => Monad m where { fmap = liftM; ... } class Functor d => Comonad d where { fmap = liftD; ... } The Id functor is an instance of Monad and Comonad; what happens to the fmap definition?
If the instance for every type were allowed, Foo and Bar would be indistinguishable from true synonyms. Further, if classes with no methods have no use currently, this "universal instance" could be compiler generated whenever a class without methods is declared. Or the empty class may be treated as a synonym, if that's simpler. Does this make any sense?
I don't know that method-less classes have *no* value. You could use them to make additional claims about a type. For example, class Monoid m where { ... } class CommutativeMonoid m where {} The idea being that instances of CommutativeMonoid satisfy additional laws. -- David Menendez <zednenem@psualum.com> | "In this house, we obey the laws <http://www.eyrie.org/~zednenem> | of thermodynamics!"
Am Donnerstag, 13. Oktober 2005 15:58 schrieb Udo Stenzel:
[...]
Further, if classes with no methods have no use currently, this "universal instance" could be compiler generated whenever a class without methods is declared.
This does mean that you want to treat classes without methods special, doesn't it? I think, that it is generally not a good thing to have special treatment for a specific case.
[...]
Udo.
Best wishes, Wolfgang
Wolfgang Jeltsch wrote:
This does mean that you want to treat classes without methods special, doesn't it?
Not quite, I'm actually not sure if I want this, I just noted that it was possible. :) As David Menendez pointed out, empty classes probably aren't that useless. Anyway, not treating empty classes specially and just making the universal instance explicit may be enough. Foo is declared an alias for Bar by writing: class Foo a => Bar a instance Foo a => Bar a The latter is not Haskell98, but a harmless extension. Doing it this way "feels better" than introducing new syntax and semantics, at least to me. Udo. -- "Don't you know that alcohol for a young man is nothing but slow poison?" "Slow poison, eh? Well, I'm not in any hurry." -- gefunden auf http://c2.com/cgi-bin/wiki?SlowPoison
G'day all. Quoting Simon Peyton-Jones <simonpj@microsoft.com>:
I've considered this before, but never done anything about it because superclasses are so close. Specifically, what is the difference between
(i) class (C a, D a) => CD a and (ii) class alias CD a = (C a, D a)
Note that (i) is Haskell 98.
To be a true typeclass synonym, there would also be an implied default instance: class (C a, D a) => CD a instance (C a, D a) => CD a ...and this is not Haskell 98. Cheers, Andrew Bromage
participants (9)
-
ajb@spamcop.net -
Benjamin Franksen -
David Menendez -
John Meacham -
Ross Paterson -
S.M.Kahrs -
Simon Peyton-Jones -
Udo Stenzel -
Wolfgang Jeltsch