Skip to content

Commit 9dab273

Browse files
authored
Merge pull request #62 from m-bock/sum
`sum` and `sumWith` (Type safe codecs for sum types with custom encodings)
2 parents d6c26da + d26e86f commit 9dab273

File tree

4 files changed

+702
-43
lines changed

4 files changed

+702
-43
lines changed

README.md

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,57 @@ If the value being decoded has no `email` field, the resulting `Person` will hav
141141

142142
This combinator only deals with entirely missing properties, so values like `null` will still need to be handled explicitly.
143143

144-
### Sum types and variants
144+
### Sum types
145145

146-
This library comes with codec support for [`purescript-variant`](https://github.com/natefaubion/purescript-variant) out of the box and codecs for sums are often based on the variant codec.
146+
Codecs for sum types can be easily defined by using the [`sum`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Sum#v:sum) function. You need to provide a record of the case constructor names, whereas each record value holds a (nested) tuple of codecs for the constructor fields.
147+
148+
Let's look at an example sum type, it has 3 constructors. The first one has zero fields, the seconds has one field and the third one has three fields.
149+
150+
```purescript
151+
data Sample
152+
= Foo
153+
| Bar Int
154+
| Baz Boolean String Int
155+
156+
derive instance Generic Sample _
157+
```
158+
159+
A simple codec for `Sample` can be created like this in a type safe way:
160+
161+
```purescript
162+
import Data.Codec.Argonaut.Sum as CAS
163+
import Data.Codec.Argonaut as CA
164+
165+
codecSample ∷ JsonCodec Sample
166+
codecSample = CAS.sum "Sample"
167+
{ "Foo": unit
168+
, "Bar": CA.int
169+
, "Baz": CA.boolean /\ CA.string /\ CA.int
170+
}
171+
```
172+
173+
The special case of a constructor with zero arguments like `Foo`, we just use `unit` instead of a tuple.
174+
175+
#### Custom encodings
176+
177+
If you need control of the actual encoding being used, there's also [`sumWith`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Sum.Sum#v:sumWith). It takes an extra argument of type [`Encoding`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Sum#v:Encoding)
178+
179+
Generally two types of encodings are supported:
180+
181+
- Nested
182+
`{"Baz": [true, "abc", 42]}`
183+
- Tagged
184+
`{"tag": "Baz", "values": [true, "abc", 42]}`
185+
186+
There are also a couple of extra options that can be specified. E.g. for custom field names instead of `"tag"` and `"value"`.
187+
188+
#### Sum types with only nullary constructors
189+
190+
If you have a sum type that only consists of nullary constructors and it has a [`Generic`](https://pursuit.purescript.org/packages/purescript-generics-rep/docs/Data.Generic.Rep#t:Generic) instance defined, [`nullarySum`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Generic#v:nullarySum) provided by [`Data.Codec.Argonaut.Generic`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Generic) can generate a codec that will encode the constructors as string values matching the constructor names in the JSON.
191+
192+
### Variant types
193+
194+
This library comes with codec support for [`purescript-variant`](https://github.com/natefaubion/purescript-variant) out of the box.
147195

148196
First of all, variants. Similar to the object/record case there are a few options for defining variant codecs, but most commonly they will be defined with [`variantMatch`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Variant#v:variantMatch) provided by [`Data.Codec.Argonaut.Variant`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Variant):
149197

@@ -179,44 +227,6 @@ The variant codec is a little opinionated since there's no exactly corresponding
179227

180228
`value` will be omitted for nullary / `Left`-defined constructors. At the moment it is not possible to customise the encoding for variant types, so they may not be suitable if you are not in control of the serialization format.
181229

182-
Sum type encoding is usually handled by building a variant codec, and then using [`dimap`](https://pursuit.purescript.org/packages/purescript-profunctor/docs/Data.Profunctor#v:dimap) to inject into/project out of a corresponding sum type:
183-
184-
```purescript
185-
import Prelude
186-
187-
import Data.Codec.Argonaut as CA
188-
import Data.Codec.Argonaut.Variant as CAV
189-
import Data.Either (Either(..))
190-
import Data.Profunctor (dimap)
191-
import Data.Variant as V
192-
import Type.Proxy (Proxy(..))
193-
194-
data SomeValue2 = Str String | Int Int | Neither
195-
196-
codec ∷ CA.JsonCodec SomeValue2
197-
codec =
198-
dimap toVariant fromVariant $ CAV.variantMatch
199-
{ str: Right CA.string
200-
, int: Right CA.int
201-
, neither: Left unit
202-
}
203-
where
204-
toVariant = case _ of
205-
Str s → V.inj (Proxy ∷ _ "str") s
206-
Int i → V.inj (Proxy ∷ _ "int") i
207-
Neither → V.inj (Proxy ∷ _ "neither") unit
208-
fromVariant = V.match
209-
{ str: Str
210-
, int: Int
211-
, neither: \_ → Neither
212-
}
213-
```
214-
215-
This certainly is a little boilerplate-y, but at least when defining codecs this way you do gain the benefits of having a single definition that aligns the encoding and decoding behaviour. This means, assuming there are no mixups in `toVariant`/`fromVariant`, the guaranteed roundtripping is preserved. Often it's not even possible to have mixups during `dimap`, since the sum constructor types will all differ.
216-
217-
If you have a sum type that only consists of nullary constructors and it has a [`Generic`](https://pursuit.purescript.org/packages/purescript-generics-rep/docs/Data.Generic.Rep#t:Generic) instance defined, [`nullarySum`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Generic#v:nullarySum) provided by [`Data.Codec.Argonaut.Generic`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Generic) can generate a codec that will encode the constructors as string values matching the constructor names in the JSON.
218-
219-
The story for sum type codecs outside of these options isn't great just now. There are some functions provided in [`Data.Codec.Argonaut.Sum`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Sum) for defining them, but these are more error prone than the variant method, and use the same encoding methods described above. Often it's just as easy to construct a codec from scratch with [`basicCodec`](https://pursuit.purescript.org/packages/purescript-codec/docs/Data.Codec#v:basicCodec) from [`Data.Codec`](https://pursuit.purescript.org/packages/purescript-codec/docs/Data.Codec), although means it's up to you to ensure the roundtrip succeeds.
220230

221231
### Other common types
222232

0 commit comments

Comments
 (0)