Purescript: Much shorter instancing/generics-deriving syntax

Created on 7 Sep 2018  Â·  3Comments  Â·  Source: purescript/purescript

This Haskell code is quite readable:

data SomethingA = Alice | Alfred | Alex deriving (Eq, Ord, Show, Bounded, Enum) 
data SomethingB = Bob | Bart            deriving (Eq, Ord, Show, Bounded, Enum) 
data SomethingC = Carol | Candice       deriving (Eq, Ord, Show, Bounded, Enum)
data SomethingD = David | Donald        deriving (Eq, Ord, Show, Bounded, Enum)

The equivalent PureScript code is basically unreadable, not to mention painful to write:

data SomethingA = Alice | Alfred | Alex
data SomethingB = Bob | Blart
data SomethingC = Carol | Candice
data SomethingD = David | Donald
derive instance _0_ ∷ Generic SomethingA _ 
derive instance _1_ ∷ Eq SomethingA 
derive instance _2_ ∷ Ord SomethingA 
instance _3_ ∷ Show SomethingA where
   show = genericShow 
instance _4_ ∷ Enum SomethingA where
   succ = genericSucc
   pred = genericPred 
instance _5_ ∷ Bounded SomethingA where
   top = genericTop
   bottom = genericBottom 
instance _6_ ∷ BoundedEnum SomethingA where
   cardinality = genericCardinality
   toEnum = genericToEnum
   fromEnum = genericFromEnum 
derive instance _7_ ∷ Generic SomethingB _ 
derive instance _8_ ∷ Eq SomethingB 
derive instance _9_ ∷ Ord SomethingB 
instance _10_ ∷ Show SomethingB where
   show = genericShow 
instance _11_ ∷ Enum SomethingB where
   succ = genericSucc
   pred = genericPred 
instance _12_ ∷ Bounded SomethingB where
   top = genericTop
   bottom = genericBottom 
instance _13_ ∷ BoundedEnum SomethingB where
   cardinality = genericCardinality
   toEnum = genericToEnum
   fromEnum = genericFromEnum 
derive instance _14_ ∷ Generic SomethingC _ 
derive instance _15_ ∷ Eq SomethingC
derive instance _16_ ∷ Ord SomethingC 
instance _17_ ∷ Show SomethingC where
   show = genericShow 
instance _18_ ∷ Enum SomethingC where
   succ = genericSucc
   pred = genericPred 
instance _19_ ∷ Bounded SomethingC where
   top = genericTop
   bottom = genericBottom 
instance _20_ ∷ BoundedEnum SomethingC where
   cardinality = genericCardinality
   toEnum = genericToEnum
   fromEnum = genericFromEnum 
derive instance _21_ ∷ Generic SomethingD _ 
derive instance _22_ ∷ Eq SomethingD 
derive instance _23_ ∷ Ord SomethingD 
instance _24_ ∷ Show SomethingD where
   show = genericShow 
instance _25_ ∷ Enum SomethingD where
   succ = genericSucc
   pred = genericPred 
instance _26_ ∷ Bounded SomethingD where
   top = genericTop
   bottom = genericBottom 
instance _27_ ∷ BoundedEnum SomethingD where
   cardinality = genericCardinality
   toEnum = genericToEnum
   fromEnum = genericFromEnum 

On top of the horrendous code bloat, _twenty-eight_ useless instance names.

A few ideas:

  • Instance names could be cut out. I know this has been brought up before and decided against, with FFI as the excuse, but it would be the simplest solution so I figure I might as well mention it. Personally, I can't imagine more than a tiny fraction of instances are written with the intention of calling them from JavaScript. Perhaps there could be some kind of manual syntax for those use cases, something like foreign export eqFoo :: Eq Foo or something? I don't really want to worry about writing code for JavaScript in PureScript any more than I want to worry about writing code for C in Haskell.

  • Bounded could be derivable, along with Enum if the library is being used. This would at least cut down some of those lines.

  • Unsure of the specifics, but Generic might be allowed to define something along the lines of

deriveable Generic a _ :: BoundedEnum a where
    cardinality = genericCardinality
    toEnum = genericToEnum
    fromEnum = genericFromEnum

that could then be used for

derive instance boundedEnumFoo :: BoundedEnum Foo

EDIT: I guess this is really about giving an "opt-in" version of default method implementations, where you have to specify that you're going to borrow the defaults, in case that's less of an ordeal to add. If default method implementations were added, that would be even better.

  • At very least, some kind of deriving shorthand along the lines of:
data Foo = Foo deriving (Eq eqFoo, Show showFoo, Ord ordFoo, Generic genericFoo)

as an equivalent to

data Foo = Foo
derive instance eqFoo :: Eq Foo
derive instance showFoo :: Show Foo
derive instance ordFoo :: Ord Foo
derive instance genericFoo :: Generic Foo _
deriving enhancement syntax

Most helpful comment

I worked on anonymous type instances and deriving clauses attached to data and newtypes declarations this week-end. With deriving via and a newtype with generic instances, this example could be written much more concisely:

data SomethingA = Alice | Alfred | Alex
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingA)

derive instance Generic SomethingA _

data SomethingB = Bob | Bart
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingB)

derive instance Generic SomethingB _

data SomethingC = Carol | Candice
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingC)

derive instance Generic SomethingC _

data SomethingD = David | Donald
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingD)

derive instance Generic SomethingD _

I’ve encountered a wall though. Deriving instances for higher kinded types requires us to know the kind of the derived type class (or at least of its last argument) in order to saturate the type constructor appropriately in the desugared instance. For example the following declaration:

newtype Identity a = Identity a
  derive (Eq, Ord)
  derive Functor

must desugar to:

newtype Identity a = Identity a

derive instance Eq a => Eq (Identity a)
derive instance Ord a => Ord (Identity a)
derive instance Functor Identity

We have to saturate Identity in the Eq and Ord instances but not in the Functor instance for the desugared instances to be well kinded. The desugaring of derived instances happens before typechecking though, so we don’t yet know any type class kind!

Is there a way to compute the kind of a type class argument before typechecking? Should we add another couple of desugaring and typechecking steps after the current typechecking step? Or perhaps we could defer the desugaring of the derived clauses and interleave it with typechecking?

All 3 comments

Relates to:

I worked on anonymous type instances and deriving clauses attached to data and newtypes declarations this week-end. With deriving via and a newtype with generic instances, this example could be written much more concisely:

data SomethingA = Alice | Alfred | Alex
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingA)

derive instance Generic SomethingA _

data SomethingB = Bob | Bart
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingB)

derive instance Generic SomethingB _

data SomethingC = Carol | Candice
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingC)

derive instance Generic SomethingC _

data SomethingD = David | Donald
  derive (Eq, Ord)
  derive (Show, Bounded, Enum) via (N SomethingD)

derive instance Generic SomethingD _

I’ve encountered a wall though. Deriving instances for higher kinded types requires us to know the kind of the derived type class (or at least of its last argument) in order to saturate the type constructor appropriately in the desugared instance. For example the following declaration:

newtype Identity a = Identity a
  derive (Eq, Ord)
  derive Functor

must desugar to:

newtype Identity a = Identity a

derive instance Eq a => Eq (Identity a)
derive instance Ord a => Ord (Identity a)
derive instance Functor Identity

We have to saturate Identity in the Eq and Ord instances but not in the Functor instance for the desugared instances to be well kinded. The desugaring of derived instances happens before typechecking though, so we don’t yet know any type class kind!

Is there a way to compute the kind of a type class argument before typechecking? Should we add another couple of desugaring and typechecking steps after the current typechecking step? Or perhaps we could defer the desugaring of the derived clauses and interleave it with typechecking?

Or perhaps we could defer the desugaring of the derived clauses and interleave it with typechecking?

This is something I noted in the PolyKinds PR. Typeclass/deriving desugaring really needs to be part of typechecking. The compiler currently has to do duplicate work to check both the desugared synonyms (which can have incomplete kind information) and the original classes/instances. Typeclass desugaring also does synonym expansion, but this also needs kind information which doesn't exist at that point.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

garyb picture garyb  Â·  3Comments

garrett-hopper picture garrett-hopper  Â·  3Comments

mpodlasin picture mpodlasin  Â·  4Comments

angadgill92 picture angadgill92  Â·  3Comments

joneshf picture joneshf  Â·  4Comments