This package was recently highlighted in Elm Weekly so I decided to give it a spin. The selling point was simultaneously defining both the encoder and decoder, which is convenient, and after enjoying using for some simple stuff, I decided to try and use it to write the encoders/decoders for some recursive types, which I’ve been dreading. The data type looks like this:
type Requirement a
= Data a
| Not (Requirement a)
| Or (Requirement a) (Requirement a)
| And (Requirement a) (Requirement a)
| Any (List (Requirement a))
| All (List (Requirement a))
elm-codec has some tools for dealing with this type of data structure. The codec, which you use to create the encoder and decoder, is made using the Codec.recursive and Codec.custom functions. Here’s the code for my codec:
requirementCodec : Codec a -> Codec (Requirement a)
requirementCodec meta =
Codec.recursive
(\rmeta ->
Codec.custom
(\fdata fnot for fand fany fall value ->
case value of
Data data ->
fdata data
Not requirement ->
fnot requirement
Or requirement1 requirement2 ->
for requirement1 requirement2
And requirement1 requirement2 ->
fand requirement1 requirement2
Any requirements ->
fany requirements
All requirements ->
fall requirements
)
|> Codec.variant1 "Data" Data meta
|> Codec.variant1 "Not" Not rmeta
|> Codec.variant2 "Or" Or rmeta rmeta
|> Codec.variant2 "And" And rmeta rmeta
|> Codec.variant1 "Any" Any (Codec.list rmeta)
|> Codec.variant1 "All" All (Codec.list rmeta)
|> Codec.buildCustom
)
It was really quick to get this up and running, the compiler (as always) held my hand. The JSON that it produces is sensible and easy to work with on the back end.
I don’t know the author or anything, just wanted to highlight a very positive experience that I had using a module!
I knew about it from Slack, and also some previous discussions on here over a year ago (Is it possible to capture type information about records?). I have been using miniBill/elm-codec in my latest project and finding it very convenient and a significant reduction in boilerplate involved in writing encoders and decoders. I would also recommend this package to anyone needing to write encoders and decoders.
I had an example where I needed mutually recursive data types (but I think the decoder only needed to be recursive in one of them).
The model on the back-end was something like this:
type ContentModle
= ContentModel
{ type _ : ContentType
, otherFields : ...
}
type ContentType
= ContentType
{ model : ContentModel
, otherFields : ...
}
It was for a CMS where the content could be made up from a tree of pieces of content. Typically a whole page at a time was requested from the server, sometimes one page would be just 1 level deep, but sometimes several levels, and the back end could recurse down and fetch 1 complete page at a time built up in this way.
I had to use custom types instead of type aliases, as a type alias does not allow mutual recursion.
Haven’t tried to write an elm-codec for this, but if its a use case you think you can handle, I think there are places where it is sometimes needed.
I suppose the only thing that was a slight pain point was needing asymmetric encoders and decoders, for example when I’m caching to local storage and I don’t need all the data on a record, where the fields that weren’t cached have default values. I ended up making a cached version of the record and made some functions to convert between, which works fine and is probably better in the long run, but I thought I’d be able to do something like this:
You can either use Codec.map to map your “memory” type to your “persistent” type and back, or use Codec.constant like this: https://ellie-app.com/5XQ6YxvRpjja1
I’ve just went through my code and replaced all the unnecessary persistent types in favour of using constant and map. It’s all worked, which is awesome, but I had a thought while doing it: Is map the right name for that function? I would have assumed it had the normal map type signature of (a -> b) -> Codec a -> Codec b, which is why I didn’t look at it too closely the first time I looked through the docs (for other readers: it has the type signature (a -> b) -> (b -> a) -> Codec a -> Codec b). Do you think it’s worth renaming this function to stop confusion and help draw attention to its use?
That’s interesting, I’ve never heard of an invariant functor before. Like you said invmap is not going to be helpful to most people, maybe a more human readable name like twoWayMap or something along those lines?