|
| 1 | +## Type Class Deriving |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +### Classes with built-in compiler support |
| 6 | + |
| 7 | +Some classes have special built-in compiler support, and their instances can be derived from all types. |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +```purs |
| 12 | +import Data.Array (nub) |
| 13 | +
|
| 14 | +data MyADT |
| 15 | + = Some |
| 16 | + | Arbitrary Int |
| 17 | + | Contents Number String |
| 18 | +
|
| 19 | +derive instance eqMyADT :: Eq MyADT |
| 20 | +derive instance ordMyADT :: Ord MyADT |
| 21 | +
|
| 22 | +nub [Some, Arbitrary 1, Some, Some] == [Some, Arbitrary 1] |
| 23 | +``` |
| 24 | + |
| 25 | +Currently, instances for the following classes can be derived by the compiler: |
| 26 | +- [Data.Generic.Rep (class Generic)](https://pursuit.purescript.org/packages/purescript-generics-rep/docs/Data.Generic.Rep#t:Generic) |
| 27 | +- [Data.Eq (class Eq)](https://pursuit.purescript.org/packages/purescript-prelude/docs/Data.Eq#t:Eq) |
| 28 | +- [Data.Ord (class Ord)](https://pursuit.purescript.org/packages/purescript-prelude/docs/Data.Ord#t:Ord) |
| 29 | +- [Data.Functor (class Functor)](https://pursuit.purescript.org/packages/purescript-prelude/docs/Data.Functor#t:Functor) |
| 30 | +- [Data.Newtype (class Newtype)](https://pursuit.purescript.org/packages/purescript-newtype/docs/Data.Newtype#t:Newtype) |
| 31 | + |
| 32 | +### Derive from `newtype` |
| 33 | + |
| 34 | +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. |
| 35 | + |
| 36 | +For example, let's say you want to add two `Score` values using the `Semiring` instance of the wrapped `Int`. |
| 37 | + |
| 38 | +```purs |
| 39 | +newtype Score = Score Int |
| 40 | +
|
| 41 | +derive newtype instance semiringScore :: Semiring Score |
| 42 | +
|
| 43 | +tenPoints :: Score |
| 44 | +tenPoints = (Score 4) + (Score 6) |
| 45 | +``` |
| 46 | + |
| 47 | +That `derive` line replaced all this code: |
| 48 | + |
| 49 | +```purs |
| 50 | +-- No need to write this |
| 51 | +instance semiringScore :: Semiring Score where |
| 52 | + zero = Score 0 |
| 53 | + add (Score a) (Score b) = Score (a + b) |
| 54 | + mul (Score a) (Score b) = Score (a * b) |
| 55 | + one = Score 1 |
| 56 | +``` |
| 57 | + |
| 58 | +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. |
| 59 | + |
| 60 | +```purs |
| 61 | +derive instance eqScore :: Eq Score |
| 62 | +derive newtype instance eqScore :: Eq Score |
| 63 | +``` |
| 64 | + |
| 65 | +### Deriving from `Generic` |
| 66 | + |
| 67 | +The compiler's built-in support for `Generic` unlocks convenient deriving for many other classes not listed above. |
| 68 | + |
| 69 | +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). |
| 70 | + |
| 71 | +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: |
| 72 | + |
| 73 | +```purescript |
| 74 | +import Data.Generic.Rep (class Generic) |
| 75 | +import Data.Generic.Rep.Show (genericShow) |
| 76 | +import Effect.Console (logShow) |
| 77 | +
|
| 78 | +derive instance genericMyADT :: Generic MyADT _ |
| 79 | +
|
| 80 | +instance showMyADT :: Show MyADT where |
| 81 | + show = genericShow |
| 82 | + |
| 83 | +main = logShow [Some, Arbitrary 1, Contents 2.0 "Three"] |
| 84 | +-- Prints: |
| 85 | +-- [Some,(Arbitrary 1),(Contents 2.0 "Three")] |
| 86 | +``` |
| 87 | + |
| 88 | +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. |
| 89 | + |
| 90 | +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: |
| 91 | + |
| 92 | +```purs |
| 93 | +import Effect.Console (logShow) |
| 94 | +
|
| 95 | +newtype Score = Score Int |
| 96 | +
|
| 97 | +-- newtype deriving omits wrapper with show |
| 98 | +derive newtype instance showScore :: Show Score |
| 99 | +
|
| 100 | +main = logShow (Score 5) |
| 101 | +-- Prints: |
| 102 | +-- 5 |
| 103 | +``` |
| 104 | + |
| 105 | +```purs |
| 106 | +import Data.Generic.Rep (class Generic) |
| 107 | +import Data.Generic.Rep.Show (genericShow) |
| 108 | +import Effect.Console (logShow) |
| 109 | +
|
| 110 | +newtype Score = Score Int |
| 111 | +
|
| 112 | +-- generic deriving prints wrapper with show |
| 113 | +derive instance genericScore :: Generic Score _ |
| 114 | +instance showScore :: Show Score where |
| 115 | + show = genericShow |
| 116 | +
|
| 117 | +main = logShow (Score 5) |
| 118 | +-- Prints: |
| 119 | +-- (Score 5) |
| 120 | +``` |
| 121 | + |
| 122 | +More information on Generic deriving is available [in the generics-rep library documentation](https://pursuit.purescript.org/packages/purescript-generics-rep). See this [blog post](https://harry.garrood.me/blog/write-your-own-generics/) for a tutorial on how to write your own `generic` functions. |
| 123 | + |
| 124 | +#### Avoiding stack overflow errors with recursive types |
| 125 | + |
| 126 | +Be careful when using generic functions with recursive data types. Due to strictness, these instances _cannot_ be written in point free style: |
| 127 | + |
| 128 | +```purs |
| 129 | +import Data.Generic.Rep (class Generic) |
| 130 | +import Data.Generic.Rep.Show (genericShow) |
| 131 | +import Effect.Console (logShow) |
| 132 | +
|
| 133 | +data Chain a |
| 134 | + = End a |
| 135 | + | Link a (Chain a) |
| 136 | +
|
| 137 | +derive instance genericChain :: Generic (Chain a) _ |
| 138 | +
|
| 139 | +instance showChain :: Show a => Show (Chain a) where |
| 140 | + show c = genericShow c -- Note the use of the seemingly-unnecessary variable `c` |
| 141 | +
|
| 142 | +main = logShow $ Link 1 $ Link 2 $ End 3 |
| 143 | +-- Prints: |
| 144 | +-- (Link 1 (Link 2 (End 3))) |
| 145 | +``` |
| 146 | + |
| 147 | +If the instance was written in point free style, then would produce a stack overflow error: |
| 148 | + |
| 149 | +``` purs |
| 150 | +instance showChain :: Show a => Show (Chain a) where |
| 151 | + show = genericShow -- This line is problematic |
| 152 | +
|
| 153 | +-- Throws this error: |
| 154 | +-- RangeError: Maximum call stack size exceeded |
| 155 | +``` |
| 156 | + |
| 157 | +This technique of undoing point free notation is known as _eta expansion_. |
| 158 | + |
0 commit comments