purescript-deprecated/purescript-generics

Instance code generation

Opened this issue · 12 comments

Is there any plan to generate this code somehow automatically?

instance showFoo :: Show Foo where
    show = gShow

instance eqFoo :: Eq Foo where
    eq = gEq

instance ordFoo :: Ord Foo where
    compare = gCompare

I don't think so. The idea of only adding generic deriving to the compiler is so we don't have to add specific cases for all the potentially derivable classes that may come along - as the prelude isn't included in the compiler there's no guarantee that Show, Eq, Ord, etc. will even exist in someone's project.

We're hoping to do something similar to generic deriving with newtype deriving though, which means you won't have to write explicit instances for newtypes at least.

But what is wrong with Haskell deriving syntax sugar?

data Foo = Foo Number String deriving( Eq, Show, Ord )

could generate all this code.
Why cannot PS have this feature?

The idea of only adding generic deriving to the compiler is so we don't have to add specific cases for all the potentially derivable classes that may come along - as the prelude isn't included in the compiler there's no guarantee that Show, Eq, Ord, etc. will even exist in someone's project.

So? Why does compiler need to know about specific cases?
We could write something like this

instance genericShow :: forall a. (Generic a) => Show a where
    show = gShow

which means generic Show implementation
And when compiler see this

data Foo = Foo Number String deriving(Show)

It search for generic Show implementation and use it to generate the instance code

instance showFoo :: Show Foo where
    show = gShow

I don't see anything specific.

The first instance would be a potential option but having an instance like that precludes the ability of writing custom Show classes for any type that you create a Generic instance for, due to instance overlaps - so you'd either have the choice of fully Generic or fully custom, the way things are now you have the option of mixing hand written and generic-based instances.

One option might be to somehow enhance class definitions to have a way of describing what an instance based on generics would look like, as that way yes, the compiler wouldn't need to know specifics. The deriving syntax would probably still be something like:

derive instance showFoo :: Show Foo

however, as explicitly named instances are here to stay.

instance like that precludes the ability of writing custom Show classes for any type that you create a Generic instance for

I don't get it. No one force you to use deriving(Show) syntax. You can always do everything manually.
If we really want flexibility even for deriving then we could use import restriction.

module M1
instance genericShow :: forall a. (Generic a) => Show a where
    show = gShow
module M2
instance otherGenericShow :: forall a. (Generic a) => Show a where
    show = gShow2
import M1
data Foo = Foo Number String deriving(Show) // genericShow
import M2
data Foo = Foo Number String deriving(Show) // otherGenericShow

That (Generic a) => Show a instance would have to be defined along with the Show class as otherwise it would be an orphan.

Yes, but you should be able to define more specific instance

instance genericShow :: forall a. (Generic a) => Show a where
    show = gShow

instance showFoo :: Show Foo where
    show = "bar"

test = show $ Foo 1 "foo"

test == "bar" because showFoo more specific than genericShow

Unfortunately that isn't how instance resolution works, the concept of "more specific" gets rather slippery when you have multi-parameter typeclasses or instances with class contexts. It may still be worth exploring, but I suspect that someone more knowledgable than me will have a reason for why it's not allowed (Haskell also doesn't work this way).

Maybe it cannot work with all scenarios, but it differently could walk with simple one like this.
There are many other possible solutions.
What about import restriction?
If there are two suitable instances

module Data.Generic
instance genericShow :: forall a. (Generic a) => Show a where
    show = gShow
module M
instance showFoo :: Show Foo where
    show = "bar"
import Data.Generic
import M
test = show $ Foo 1 "foo"

Compiler will force you to hide one instance

import Data.Generic hiding (genericShow)
import M
test = show $ Foo 1 "foo"
gbaz commented

A syntax proposal from the channel:

derivable class Generic a => Show a where
       show = gShow

instance showFoo :: Show Foo

i.e. a "derivable class" is sort of like a "default instance"

If you declare one -- and I guess really only one can exist per class, then if you declare an instance without methods, the derivable instance, if it matches, will fill in the instances for you...

Not arguing for this now or ever necessarily, but recording it for posterity as a sane approach.

@garyb

there's no guarantee that Show, Eq, Ord, etc. will even exist in someone's project.

I think there is. For example in haskell -XDerivingGeneric wouldn't allow you to say deriving Generic unless you import Data.Generic.

The problem is of course (as you said earlier) that if someone defines their own Show compiler might not be able to figure out that it's not the Show that we were looking for.

We could go a bit duck-typish here, and do something like

  • If the user asks to derive (Show), check that
    • Show is in the scope
    • Show in the scope, is exactly the same as the Show which we are thinking about, namely
      • has a single * in the head
      • has one method show :: a -> String
  • and if all that holds true, derive a Show instance

Becomes kind of ugly and complicated though.