Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit 36648d4

Browse files
committed
Add guide by Phil Freeman verbatim
1 parent 49ba119 commit 36648d4

File tree

1 file changed

+138
-2
lines changed

1 file changed

+138
-2
lines changed

README.md

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,142 @@ This library requires version 0.10 of the PureScript compiler, or later.
1313
bower install purescript-generics-rep
1414
```
1515

16-
## Documentation
16+
## Introduction
1717

18-
Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-generics-rep).
18+
The PureScript compiler has supported _generic deriving_ in some form since version 0.7.3, thanks to [Gershom Bazerman](http://gbaz.github.io/). However, since then, it has gone through several iterations, and the current version (implemented in the `purescript-generics-rep` library) makes it possible to have the compiler derive a wide variety of boilerplate code based on types. One example of this is the serialization and deserialization of JSON, and in this post I'll show how to use the [`purescript-foreign-generic`](https://github.com/paf31/purescript-foreign-generic) library to create such functions using generics.
19+
20+
### Generics Overview
21+
22+
PureScript's generics are supported by the `purescript-generics-rep` library, and in particular, the `Data.Generic.Rep.Generic` type class:
23+
24+
```purescript
25+
class Generic a rep | a -> rep where
26+
from :: a -> rep
27+
to :: rep -> a
28+
```
29+
30+
There are three interesting things here:
31+
32+
- The `rep` type argument and associated functional dependency define a type function from user types (`a`) to their _representations_ (`rep`).
33+
- `from` converts a regular value into the representation type.
34+
- `to` converts a representation value back into a regular value.
35+
36+
`purescript-generics-rep` provides standard representation types which can be used to represent any `data` types which can be expressed in PureScript code.
37+
38+
It is possible to write out `Generic` instances for our own types by hand, but doing so is very laborious. Instead, we can _derive_ instances by using the `derive` keyword:
39+
40+
```purescript
41+
newtype Person = Person { name :: String, location :: String }
42+
43+
derive instance genericPerson :: Generic Person _
44+
```
45+
46+
Note that the second type argument, which represents the representation type, is specified as a type wildcard. This is useful, because representation types can get quite large, so it is inconvenient to type them out by hand in deriving declarations.
47+
48+
### Show, Eq, Ord
49+
50+
The key insight regarding generics is this: if we can write a function which works with any of the standard representation types, then we implement the same function for any instance of `Generic`. We can even exploit type information in our implementation by using additional type classes to reflect the type information at runtime.
51+
52+
`purescript-generics-rep` provides helper functions for implementing common type classes from the Prelude:
53+
54+
- `genericShow` gives a default implementation of `show` from the `Show` class
55+
- `genericEq` gives a default implementation of `eq` from the `Eq` class
56+
- `genericCompare` gives a default implementation of `compare` from the `Ord` class
57+
- `genericAppend` gives a default implementation of `append` from the `Semigroup` class
58+
- `genericMempty` gives a default implementation of `mempty` from the `Monoid` class
59+
60+
Using these functions is as simple as dropping the generic implementation into your instances:
61+
62+
```purescript
63+
instance showPerson :: Show Person where
64+
show = genericShow
65+
66+
instance eqPerson :: Eq Person where
67+
eq = genericEq
68+
69+
instance ordPerson :: Ord Person where
70+
compare = genericCompare
71+
72+
instance semigroupPerson :: Semigroup Person where
73+
append = genericAppend
74+
```
75+
76+
### Handling Foreign Data
77+
78+
The `purescript-foreign` library is used in PureScript to handle untrusted external data, and to turn such data into typed values. This functionality is represented by the `Decode` type class in the `purescript-foreign-generic` library:
79+
80+
```purescript
81+
class Decode a where
82+
decode :: Foreign -> F a
83+
```
84+
85+
`Decode` instances are a good example of boilerplate code. In most cases, we proceed based on the structure of the type in question. For example, here is one possible implementation of `Decode` for our `Person` type, using the new _field puns_ feature:
86+
87+
``` haskell
88+
instance decodePerson :: Decode Person where
89+
decode value = do
90+
name <- value ! "name"
91+
location <- value ! "location"
92+
pure $ Person { name, location }
93+
```
94+
95+
This is not too bad, but real-world records often contain many more fields. Also, it would be nice if we could be sure that the corresponding _encoding_ function would always generate compatible data. Let's see how to verify the same data using `Generic`, which will solve both of these problems.
96+
97+
The `purescript-foreign-generic` library defines a function `genericDecode`, with the following type:
98+
99+
``` haskell
100+
genericDecode
101+
:: forall a rep
102+
. Generic a rep
103+
=> GenericDecode rep
104+
=> Options
105+
-> Foreign
106+
-> F a
107+
```
108+
109+
The `Options` type here is based on the options record from Haskell's `aeson` library. For our purposes, the default options will work, but we need to turn on the `unwrapSingleConstructors` option, so that our `newtype` constructor gets ignored during serialization:
110+
111+
``` haskell
112+
myOptions :: Options
113+
myOptions = defaultOptions { unwrapSingleConstructors = true }
114+
```
115+
116+
With this, our `Decode` instance is as simple as:
117+
118+
``` haskell
119+
instance decodePerson :: Decode Person where
120+
decode = genericDecode myOptions
121+
```
122+
123+
We can test out this instance in PSCi as follows:
124+
125+
```text
126+
> import Data.Generic.Rep
127+
> import Data.Generic.Rep.Show
128+
129+
> map genericShow (decodeJSON "{ 'name': 'John Smith', 'location': 'USA' }" :: Either ForeignError Person)
130+
Right (Person { name: "John Smith", location: "USA" })
131+
```
132+
133+
### Generating JSON
134+
135+
Just as `genericDecode` can be used to _read_ well-typed data, the `genericEncode` and `genericEncodeJSON` functions can be used to produce the appropriate data or JSON from a PureScript value. The generated `genericDecode` and `genericEncode` functions are inverse to each other for any given input type.
136+
137+
In PSCi, we can test JSON generation for our `Person` data type:
138+
139+
```text
140+
> encodeJSON (Person { name: "John Smith", location: "USA" })
141+
"{ 'name': 'John Smith', location: 'USA' }"
142+
```
143+
144+
One application of this technique is to produce and consume JSON for use with JSON web services, using generics to reduce the amount of boilerplate model code needed.
145+
146+
### Performance Concerns
147+
148+
Generic deriving can be very convenient for code generation, but it comes with a performance penalty. Consider defining a `Show` instance using `genericShow` - instead of simply converting our data type directly to a `String`, we first convert it to the representation type, and then convert that representation into a `String`. Creating this intermediate structure comes with a cost.
149+
150+
Thankfully, the `generics-rep` approach means that we only need to perform a shallow copy of the data, up to the first data constructor or record, so in practice the performance cost is acceptable. In the case of `foreign-generic`, the benefits listed above usually outweight the performance cost, since we rarely need to parse or generate JSON in performance-critical sections of code in many applications.
151+
152+
## API Documentation
153+
154+
API documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-generics-rep).

0 commit comments

Comments
 (0)