The compiler can derive type class instances to spare you the tedium of writing boilerplate. There are a few ways to do this depending on the specific type and class being derived.
Some classes have special built-in compiler support, and their instances can be derived from all types.
For example, if you you'd like to be able to remove duplicates from an array of an ADT using nub
, you need an Eq
and Ord
instance. Rather than writing these manually, let the compiler do the work.
import Data.Array (nub)
data MyADT
= Some
| Arbitrary Int
| Contents Number String
derive instance eqMyADT :: Eq MyADT
derive instance ordMyADT :: Ord MyADT
nub [Some, Arbitrary 1, Some, Some] == [Some, Arbitrary 1]
Currently, instances for the following classes can be derived by the compiler:
- Data.Generic.Rep (class Generic) see below
- Data.Eq (class Eq)
- Data.Ord (class Ord)
- Data.Functor (class Functor)
- Data.Newtype (class Newtype)
If you would like your newtype to defer to the instance that the underlying type uses for a given class, then you can use newtype deriving via the derive newtype
keywords.
For example, let's say you want to add two Score
values using the Semiring
instance of the wrapped Int
.
newtype Score = Score Int
derive newtype instance semiringScore :: Semiring Score
tenPoints :: Score
tenPoints = (Score 4) + (Score 6)
That derive
line replaced all this code:
-- No need to write this
instance semiringScore :: Semiring Score where
zero = Score 0
add (Score a) (Score b) = Score (a + b)
mul (Score a) (Score b) = Score (a * b)
one = Score 1
Note that we can use either of these options to derive an Eq
instance for a newtype
, since Eq
has built-in compiler support. They are equivalent in this case.
derive instance eqScore :: Eq Score
derive newtype instance eqScore :: Eq Score
The compiler's built-in support for Generic
unlocks convenient deriving for many other classes not listed above.
For example, if we wanted to derive a Show
instance for MyADT
it might seem like we're out of luck: Show
is not a class with built-in compiler support for deriving and MyADT
is not a newtype
(so we can't use newtype deriving).
But we can use genericShow
, which works with any type that has a Generic
instance. And recall that the compiler has built-in support for deriving a Generic
instance for any type (including the MyADT
type). We put all those pieces together like so:
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
import Effect.Console (logShow)
derive instance genericMyADT :: Generic MyADT _
instance showMyADT :: Show MyADT where
show = genericShow
main = logShow [Some, Arbitrary 1, Contents 2.0 "Three"]
-- Prints:
-- [Some,(Arbitrary 1),(Contents 2.0 "Three")]
The Show
type class is most often used for debugging data, so the output of most Show
instances can be copy-pasted back into a PureScript source file to reconstruct the original data. The Show
instance we created by deriving Generic
and then using genericShow
follows this convention.
This is a good opportunity to emphasize how newtype deriving is different from instances derived by the compiler or through the Generic
type class. In the examples below, notice how the instance derived through Generic
includes the newtype constructor Score
, but the newtype-derived instance simply reuses the underlying Show
instance for Int
and therefore does not include the constructor:
import Effect.Console (logShow)
newtype Score = Score Int
-- newtype deriving omits wrapper with show
derive newtype instance showScore :: Show Score
main = logShow (Score 5)
-- Prints:
-- 5
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
import Effect.Console (logShow)
newtype Score = Score Int
-- generic deriving prints wrapper with show
derive instance genericScore :: Generic Score _
instance showScore :: Show Score where
show = genericShow
main = logShow (Score 5)
-- Prints:
-- (Score 5)
You can try out generic deriving in the example at Try PureScript. See also Jordan's Reference for more discussion, and this blog post for a tutorial on how to write your own generic
functions (NB there have been language and library changes since that post).
Be careful when using generic functions with recursive data types. Due to strictness, these instances cannot be written in point free style:
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
import Effect.Console (logShow)
data Chain a
= End a
| Link a (Chain a)
derive instance genericChain :: Generic (Chain a) _
instance showChain :: Show a => Show (Chain a) where
show c = genericShow c -- Note the use of the seemingly-unnecessary variable `c`
main = logShow $ Link 1 $ Link 2 $ End 3
-- Prints:
-- (Link 1 (Link 2 (End 3)))
If the instance was written in point free style, then would produce a stack overflow error:
instance showChain :: Show a => Show (Chain a) where
show = genericShow -- This line is problematic
-- Throws this error:
-- RangeError: Maximum call stack size exceeded
This technique of undoing point free notation is known as eta expansion.