Learning Data.Generic

This might be a good thread for examples of using Data.Generic.

I’ll begin with a question. I’m at a yak-shaving point where I want to define Eq on this sum type:

data Fill
  = Solid Color
  | LinearGradient Color Color Percent
  | RadialGradient Color Color Point
  | NoFill

That means I need Eq for the various component types. Percent is easy:

newtype Percent = Percent Number
derive instance genericPercent :: Generic Percent
instance eqPercent :: Eq Percent where
 eq = gEq

But Color (from purescript-colors) presents a problem. It has Eq, but it seems unavailable. This:

derive instance genericFill :: Generic Fill    

… produces this:

  No type class instance was found for
    Data.Generic.Generic Color
while applying a function toSpine
  of type Generic t0 => t0 -> GenericSpine
  to argument $5
while checking that expression toSpine $5
  has type GenericSpine
in value declaration genericFill

I did look at https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md, but I can’t tell if anything there applies.

Various flailing around with newtypes hasn’t helped.

1 Like

You could use

derive instance eqFill :: Eq Fill

assuming you already had Eq Color, etc. implemented.

To use Data.Generic, derive each instance in turn:

derive instance genericFill :: Generic Fill
derive instance genericColor :: Generic Color
...

Note that support for Data.Generic will be removed from the compiler in 0.12.

Also important is that Data.Generic is deprecated and replaced with Generics-Rep https://github.com/purescript/purescript-generics-rep

And as you’ve run into, Color doesn’t have Data.Generic.Generic instance, and I don’t think this is fixable afaik as Data.Generic.Generic tries to traverse the whole structure at once. edit: I guess this is not supposed to be the case. See below.

The solution is just to use the new Generics-Rep instead. See the tests in Generics-Rep for examples: https://github.com/purescript/purescript-generics-rep/blob/master/test/Main.purs

It seems worthy to update the readme for the generics library to point to generics-rep and note it’ll be deprecated in 0.12. When I first started working with generics I assumed that it was the default.

Yeah, I haven’t noticed that difference between the two implementations of Generic before. It’s pretty interesting that the Data.Generic.Rep (Generic) doesn’t require that types inside the to-be-made generic type themselves have instances of Data.Generic.Rep (Generic). I wonder why that is!

import Data.Generic as G
import Data.Generic.Rep as Rep

data Foo = Foo
data Bar = Bar Foo

-- derive instance genericBar :: G.Generic Bar
-- Fails type-checking
derive instance genericRepBar :: Rep.Generic Bar _
-- Passes type-checking

Actually, that was one of the main motivations from moving away from Data.Generic. We don’t want the appearance of things like DateTime or Map, which don’t (and shouldn’t!) have Generic instances to prevent us from defining Generic on our models.

There are other advantages to the Generics.Rep approach. They’re explained really well here.

3 Likes

It appears that the way to use Data.Generic.Rep with Color is like this:

newtype Choler = Choler Color
derive instance genericCholer :: Generic Choler _
instance eqCholer :: Eq Choler where
  eq = GEq.genericEq  

If I try to work with Color directly, as in this:

derive instance genericColor :: Generic Color _

… I get the “T is [not] a type constructor defined in the same module” error.


Similarly, to work with a record, it seems I also have to wrap it:

newtype Point = Point { x :: Number, y :: Number}

derive instance genericPoint :: Generic Point _
instance eqPoint :: Eq Point where
  eq = GEq.genericEq

I suspect that the answer to “how do I define Eq on an unwrapped record?” is: “You don’t want to do that” rather than “like this: …” Yes?

This will be part of the 0.12 changes. Record will have an Eq instance, but also for Generic.Rep, RowToList and Record instances will be the way to handle generic record things, so it should all “just work”.

That is the answer, but here is an explanation. There is no Eq (Record r) instance right now - there can’t be because it would need RowToList which isn’t in Prelude, and the orphan instance restriction forces it to live with either Eq or Record (and Record is defined in Prim). Finally, you can’t define Eq (Record some_row_you_defined) because of the restriction on rows in instance heads. Allowing rows in instance heads would complicate type inference and run counter to the point of row polymorphism (that labels in rows have no globally-defined meaning).

As Nathan pointed out, this will all be addressed in 0.12 by moving RowToList into Prelude and defining one global Eq (Record r) instance.

3 Likes