elm-community / json-extra

Convenience functions for working with Json.
http://package.elm-lang.org/packages/elm-community/json-extra/latest
MIT License
37 stars 16 forks source link

andThen2 : (a -> b -> Decoder c) -> Decoder a -> Decoder b -> Decoder c #25

Open skyqrose opened 5 years ago

skyqrose commented 5 years ago

For combining multiple decode results in a way that might fail.

E.g if you have json that looks like

{
  "ids": [1, 2, 3, 4],
  "names": ["Not", "Enough", "Names"]
}

and you want to zip the two lists together, but fail if they're different lengths, you could do

Decode.Extra.andThen2
    (\ids names ->
        if List.length ids == List.length names
            Decode.succeed (List.zip ids names)
        else
            Decode.fail "expected the same number of ids and names"
    )
    (field "ids" (list int))
    (field "names" (list string))

Can be implemented as something like

andThen2 : (a -> b -> Decoder c) -> Decoder a -> Decoder b -> Decoder c
andThen2 f decoderA decoderB =
    map2 Tuple.pair decoderA decoderB
        |> andThen (\(a, b) -> f a b)

It's simple enough to use the tuple everywhere, but it reads a little better to be able to say andThen2 instead.

zwilias commented 5 years ago

An alternative implementation (and also a way to work around the lack of this) is with something like map2 f decoderA decoderB |> andThen identity, where andThen identity is a way to implement join : Decoder (Decoder a) -> Decoder a.

In my personal projects, I prefer using a pipeline-style of decoding, so I'm likely to end up writing this like so:

decoder : Decoder (List ( Int, String ))
decoder =
    Decode.succeed zip
        |> Decode.andMap (Decode.field "ids" (Decode.list Decode.int))
        |> Decode.andMap (Decode.field "names" (Decode.list Decode.string))
        |> Decode.andThen identity

-- Couldn't help myself and had to turn this into a tail recursive thing
zip : List a -> List b -> Decoder (List ( a, b ))
zip left right =
    zipHelper left right []

zipHelper : List a -> List b -> List ( a, b ) -> Decoder (List ( a, b ))
zipHelper left right acc =
    case ( left, right ) of
        ( [], [] ) ->
            Decode.succeed (List.reverse acc)

        ( x :: xs, y :: ys ) ->
            zipHelper xs ys (( x, y ) :: acc)

        ( _, _ ) ->
            Decode.fail "Expected lists of equal length"

So, I'm torn. On the one hand, it takes some insight to realize map2 + join is andThen2 (in the same way that map + join = andThen), whereas andThenX is fairly obvious. On the other hand, adding a join at the end makes this easily usable with all mapX and andMap sort of deals, without needing a whole bunch of functions to cover the spectrum. Finally, there is precedent for having a join function for pipeline style decoding 🤔

Long story short, I'm curious how knowing about the andThen identity trick might change your thoughts on this?

mgold commented 5 years ago

map2 Tuple.pair is how I would implement it. Would we want andThen3 and so on?