Encoding/Decoding Recursive Types with miniBill/elm-codec

I just want to tell people about my experience using the elm-codec package made by miniBill:

https://package.elm-lang.org/packages/miniBill/elm-codec/latest/

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!

10 Likes

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.

4 Likes

Took me a while between that posts and publishing the library, eh? :joy:

@ChrisWellsWood: thanks for the positivity! Any pain points or missing features?

One thing that I totally need to add is support for mutually recursive data types. (I was thinking along the lines of recursive2)

3 Likes

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.

1 Like

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:

storedCodec : Codec Specification
storedCodec =
    Codec.object
        (\stored ->
            { name = stored.name
            , description = stored.description
            , requirements = stored.requirements
            , deleteStatus = Common.Unclicked
            }
        )
        |> Codec.field "name" .name Codec.string
        |> Codec.field "description" .description Codec.string
        |> Codec.field "requirements" .requirements (requirementCodec requirementDataTypeCodec)
        |> Codec.buildObject
1 Like

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

2 Likes

Ah, this is great! I knew there must be a way.

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?

1 Like

Technically Codec is an Invariant Functor, and map should be called invmap: https://hackage.haskell.org/package/invariant-0.5.3/docs/Data-Functor-Invariant.html#v:invmap
Ototh I don’t think that invmap would be more helpful that map as a name.

I must definitely document the constant and map examples tho.

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?

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.