Opened 8 years ago

Last modified 5 years ago

#2895 new feature request

Implement the "Class System Extension" proposal

Reported by: porges Owned by:
Priority: normal Milestone:
Component: Compiler Version: 6.10.1
Keywords: proposal Cc: bgamari@…
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:



Executive summary:

  1. Class and instance declarations would allow method implementations to be given for any methods in the class or any ancestor class.
  2. Whenever an instance declaration is visible there would always be a full set of instance declarations for all ancestor classes, by supplementing the set of explicitly given instance declarations that are visible in a module by automatically generated implicit instance declarations.
  3. The most specific method implementation would always be chosen (ie prefer an explicit instance method over a class method and prefer a subclass method to a superclass method)
  4. Modules would only export explicit instance declarations

Change History (3)

comment:1 Changed 8 years ago by simonpj

difficulty: Unknown
Milestone: _|_

There seems to be a lot of overlap between

although I don't think they are quite the same. It's hard to be sure because they are written at very different levels of detail.

I gather that the basic idea is to allow

class D a where
   opD :: ...

class D a => C a where
   opC :: ...
   opC = ...

   opD = ...opC...

instance C Int where
   opC = ...

This leaves me with lots of questions:

  • The above instance declaration for C implies an implicit instance declaration for D:
    instance D Int where
       opD = ...opC...
    But what triggers that behaviour? Is it the mere existence of a single default method for D in the class declaration for C?
  • There is no textual clue that we get an instance for D. Shouldn't there be? As the other proposal suggests, perhaps
    instance (C Int, D Int) where 
       opC = ...
    But that is bad in another way: now we can't re-factor C into two classes without changing the instances of C, which was one of John Meacham's specific goals.
  • What if there already is an instance for D? Is that ok? If an existing explicit instance somehow suppresses the implicit instance, there is even more going on in invisible implicit-land. What if the explicit instance was not exactly the same, but merely overlapped?

I think a good way to make the proposal(s) more precise would be to give a translation into ordinary Haskell. That would answer some of these questions.

Interesting stuff here, but it needs a bit of work.


comment:2 Changed 6 years ago by batterseapower

Type of failure: None/Unknown

I think the answers to your questions are:

  • What triggers the generation of an instance using superclass defaults is exactly the same as what triggers an error message like this from GHC:
    No instance for (Functor Maybe)
      arising from the superclasses of an instance declaration
    Possible fix: add an instance declaration for (Functor Maybe)
    In the instance declaration for `Monad Maybe'

i.e. you get a generated instance only if GHC can't already find one. So if I add a (Functor m) constraint to the Monad class in base and the user has already declared a Functor instance we keep that one. In particular, this means that if the existing instance is overlapped (e.g. we have in scope an instance (Functor a) for any a) then we use that one instead of generating a new, more specific, instance.

  • There is indeed no textual clue that you get a D instance, but we just live with that

Here are two more problems though:

  1. What do we do about disambiguation?
class Foo a where
    foo :: a -> String

class (Foo a, Foo b) => Bar a b where
    bar :: Either a b -> String
    foo x = bar $ Left x

    foo x = bar $ Right x

I would argue that it should be a type error because the RHSes for foo do not have the same type. However, as an extension we could allow this:

class Foo a where
    foo :: a -> String

class (Foo a, Foo b) => Bar a b where
    bar :: Either a b -> String
    (Foo a).foo = bar . Left

    (Foo b).foo = bar . Right

Allowing qualified names like this is in the spirit that the variables on the LHS of class/instance method declarations are much more like references to variables than declarations of them. Neatly, this does not conflict with existing syntax because programs like this one are rejected (GHC says "Qualified name in binding position"):

import Control.Applicative
import qualified Data.Traversable as T

data I a = I a

instance T.Traversable I where
    T.traverse f (I x) = pure I <*> f x

Alternative syntax for disambiguation could use explicit type signatures (less nicely IMHO):

class Foo a where
    foo :: a -> String

class (Foo a, Foo b) => Bar a b where
    bar :: Either a b -> String
    foo :: a -> String
    foo = bar . Left

    foo :: b -> String
    foo = bar . Right

Both of these have their own issues (parsing in the first case, the necessity of using unification to disambiguate in the second case).

  1. Could we relax the requirements above to admit more programs?
class Baz a where
    baz1 :: a -> String
    baz2 :: a -> String

class Baz a => Quxx a where
    quxx :: a -> (String, String)
    baz1 = fst . quxx
    baz2 = snd . quxx

instance Baz (String, String) where
    baz1 = "Explicit1"

instance Quxx (String, String) where
    quxx = id

main = print $ baz2 ("Implicit1", "Implicit2")

Under the rules I gave above, this is a compile-time error because we have not given a definition for baz2 in our Baz instance, and we don't get a Baz instance from superclass defaulting because one has been explicitly declared. However, we could potentially "look ahead" to the Quxx definition and use the superclass default to give a baz2 implementation, thus printing "Implicit2" here.

comment:3 Changed 5 years ago by bgamari

Cc: bgamari@… added
Note: See TracTickets for help on using tickets.