json-extra icon indicating copy to clipboard operation
json-extra copied to clipboard

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

Open skyqrose opened this issue 6 years ago • 2 comments

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.

skyqrose avatar Aug 03 '19 16:08 skyqrose

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?

zwilias avatar Aug 13 '19 07:08 zwilias

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

mgold avatar Aug 15 '19 02:08 mgold