Discussion:
[elm-discuss] Missing json decoding tool to decode multitouch events cleanly
Matthieu Pizenberg
2017-05-06 09:00:11 UTC
Permalink
Hi all,

I'm currently rewriting my multitouch package (mpizenberg/elm-touch-events
<http://package.elm-lang.org/packages/mpizenberg/elm-touch-events/latest>)
to provide a much simpler API
<https://github.com/mpizenberg/elm-touch-events/issues/1>. While doing so,
I am also trying to clean the code. And one thing that is very hacky is the
way I'm decoding the touches for multitouch. Basically, the JS object value
to decode <https://developer.mozilla.org/en-US/docs/Web/API/TouchList> is
of the form:

{ length: Int, item: (function), "0": Touch, "1": Touch, ..., "n-1": Touch }

where n is the number of fingers touching the surface (so it's also the
value of the int `length` attribute).

Figuring out this form of the TouchList object was already a challenge
since getting it as an `Encode.Value` just create a runtime error. The key
function that made this available is the combination of `Json.Decode.maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#maybe>`
function with `Json.Decode.dict
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#dict>
`.

maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#maybe>
: Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
a -> Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
(Maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Maybe#Maybe> a)
dict
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#dict>
: Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
a -> Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
(Dict <http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Dict#Dict>
String a)

figureStructureDecoder : Decoder (Dict String Bool)
figureStructureDecoder =
Decode.dict (Decode.succeed True)

type alias Touch = ... -- some type for decoding a JS Touch object
touchDecoder : Decoder Touch

tryDecodeTouches : Decoder (Dict String (Maybe Touch))
tryDecodeTouches =
Decode.maybe touchDecoder
|> Decode.dict

Using the `figureStructureDecoder` decoder enabled me to figure out Touch
JS object structure. Then, using the `tryDecodeTouches` with different
variations of `touchDecoder` I managed to get a an elm record of the form:

{ "length": Nothing, "item": Nothing, "0": Just Touch, ..., "n-1": Just
Touch }

In the end, retrieving all the touches is just a matter of filtering out
the `Nothing`.

Now this is obviously not very clean. I've been trying to use
*`Decode.andThen`* by first decoding the `length` attribute (to get `n`)
and then trying to decode all the "0", ... "n-1" attributes by creating a
list of dedicated decoders (each using the approriate `Decode.field`
function). The problem is now I get a *`List (Decoder Touch)`* and no way
to make that a *`Decoder (List Touch)`*.

I've tried to use the *`Json.Extra.sequence`* function:

sequence : List (Decoder a) -> Decoder (List a)

But I couldn't figure out how to make it work. Actually it's because of
this mention: "Note that this function expects the list of decoders to have
the same length as the list of values in the JSON". Well, ... first I don't
have a list of values but one value and a list of decoders for different
objects inside that value. And by looking at the code, I realized that it
also relies on the use of *`Decode.value`*, which in this case would create
a runtime exception anyway as I explained earlier.

So in the end, I can't figure out a cleaner way to decode a multitouch
event. *Any ideas or tips?*
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Max Goldstein
2017-05-06 16:12:31 UTC
Permalink
What about something like this?

decodeTouches : Decoder (List Touch)
decodeTouches =
Decode.field "length" Decode.int
|> Decode.andThen
(\len ->
case len of
1 ->
Decode.map (\a -> [ a ])
(Decode.field "1" touchDecoder)

2 ->
Decode.map2 (\a b -> [ a, b ])
(Decode.field "1" touchDecoder)
(Decode.field "2" touchDecoder)

3 ->
Decode.map3 (\a b c -> [ a, b, c ])
(Decode.field "1" touchDecoder)
(Decode.field "2" touchDecoder)
(Decode.field "3" touchDecoder)

_ ->
Decode.fail "Unexpected length"
)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
art yerkes
2017-05-06 18:49:56 UTC
Permalink
Something to note about event objects in javascript is that they can go out
of scope when you carry them away from the event handler, consequently, elm
does all decoding on the same stack as the event, and you're likely to get
a runtime error if you store the raw event value.

I decided to write a bit about the topic of hairy decoders:

https://medium.com/@prozacchiwawa/the-im-stupid-elm-language-nugget-16-295f201eb458

Using a Dict to enumerate all keys can work as you see, but you're also
exposed to strange values (such as the "item" function, and elm doesn't
like function values much).

Kind of wrote this as well because, despite that there's nothing super
wrong with it in the case of touch events (hopefully it only goes to 10),
my programmer sense goes off matching length to constants as in Max
Goldstein's reply.

TLDR for here: you can easily capture a Json.Decode.Value to use as part of
a decoder, as long as you take your info from it in the same stack, and
from there, you can convert code that uses map, andThen and Ok as results
to Json.Decode.Decoder.
Post by Max Goldstein
What about something like this?
decodeTouches : Decoder (List Touch)
decodeTouches =
Decode.field "length" Decode.int
|> Decode.andThen
(\len ->
case len of
1 ->
Decode.map (\a -> [ a ])
(Decode.field "1" touchDecoder)
2 ->
Decode.map2 (\a b -> [ a, b ])
(Decode.field "1" touchDecoder)
(Decode.field "2" touchDecoder)
3 ->
Decode.map3 (\a b c -> [ a, b, c ])
(Decode.field "1" touchDecoder)
(Decode.field "2" touchDecoder)
(Decode.field "3" touchDecoder)
_ ->
Decode.fail "Unexpected length"
)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Erik Lott
2017-05-06 22:49:41 UTC
Permalink
touchList : Json.Decoder a -> Json.Decoder (List a)
touchList decoder =
let
toTouchKeysLoop num count accum =
if count < num then
toTouchKeysLoop num (count + 1) (accum ++ [ toString count
])
else
accum

toTouchKeys num =
toTouchKeysLoop num 0 []

decodeTouchValues =
List.map (\key -> Json.field key decoder)
Post by Matthieu Pizenberg
List.foldr (Json.map2 (\a accum -> a :: accum))
(Json.succeed [])
in
Json.field "length" Json.int
|> Json.map toTouchKeys
|> Json.andThen decodeTouchValues

Full gist
here: https://gist.github.com/eriklott/bb78ae787d94e0157befc480fbd6b06e#file-touch-elm
Post by Matthieu Pizenberg
Hi all,
I'm currently rewriting my multitouch package (mpizenberg/elm-touch-events
<http://package.elm-lang.org/packages/mpizenberg/elm-touch-events/latest>)
to provide a much simpler API
<https://github.com/mpizenberg/elm-touch-events/issues/1>. While doing
so, I am also trying to clean the code. And one thing that is very hacky is
the way I'm decoding the touches for multitouch. Basically, the JS object
value to decode
<https://developer.mozilla.org/en-US/docs/Web/API/TouchList> is of the
{ length: Int, item: (function), "0": Touch, "1": Touch, ..., "n-1": Touch }
where n is the number of fingers touching the surface (so it's also the
value of the int `length` attribute).
Figuring out this form of the TouchList object was already a challenge
since getting it as an `Encode.Value` just create a runtime error. The key
function that made this available is the combination of `Json.Decode.maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#maybe>`
function with `Json.Decode.dict
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#dict>
`.
maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#maybe>
: Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
a -> Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
(Maybe
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Maybe#Maybe> a)
dict
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#dict>
: Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
a -> Decoder
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#Decoder>
(Dict <http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Dict#Dict>
String a)
figureStructureDecoder : Decoder (Dict String Bool)
figureStructureDecoder =
Decode.dict (Decode.succeed True)
type alias Touch = ... -- some type for decoding a JS Touch object
touchDecoder : Decoder Touch
tryDecodeTouches : Decoder (Dict String (Maybe Touch))
tryDecodeTouches =
Decode.maybe touchDecoder
|> Decode.dict
Using the `figureStructureDecoder` decoder enabled me to figure out Touch
JS object structure. Then, using the `tryDecodeTouches` with different
{ "length": Nothing, "item": Nothing, "0": Just Touch, ..., "n-1": Just
Touch }
In the end, retrieving all the touches is just a matter of filtering out
the `Nothing`.
Now this is obviously not very clean. I've been trying to use
*`Decode.andThen`* by first decoding the `length` attribute (to get `n`)
and then trying to decode all the "0", ... "n-1" attributes by creating a
list of dedicated decoders (each using the approriate `Decode.field`
function). The problem is now I get a *`List (Decoder Touch)`* and no way
to make that a *`Decoder (List Touch)`*.
sequence : List (Decoder a) -> Decoder (List a)
But I couldn't figure out how to make it work. Actually it's because of
this mention: "Note that this function expects the list of decoders to have
the same length as the list of values in the JSON". Well, ... first I don't
have a list of values but one value and a list of decoders for different
objects inside that value. And by looking at the code, I realized that it
also relies on the use of *`Decode.value`*, which in this case would
create a runtime exception anyway as I explained earlier.
So in the end, I can't figure out a cleaner way to decode a multitouch
event. *Any ideas or tips?*
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Matthieu Pizenberg
2017-05-08 01:27:12 UTC
Permalink
Post by Max Goldstein
What about something like this?
Max, regarding the "case" solution, it was actually something I wanted to
avoid. It's definitively working, and I don't think handling more than 8
touch points is either worth it or even correctly managed by hardwares.
It's just that I don't feel good when seeing hardcoded case handling ^^.


TLDR for here: you can easily capture a Json.Decode.Value to use as part of
Post by Max Goldstein
a decoder, as long as you take your info from it in the same stack, and
from there, you can convert code that uses map, andThen and Ok as results
to Json.Decode.Decoder.
Thanks a lot Art for this write up. I never really thought of the
`decodeValue` trick to manage such a case. It's a handy tool to keep in
mind! I also didn't know about the "same stack" thing!
Post by Max Goldstein
https://gist.github.com/eriklott/bb78ae787d94e0157befc480fbd6b06e#file-touch-elm
Using `Json.Decode.map2 (::)` as the accumulator function of a `fold` to
build a new `Decoder (List a)` is pretty neat! However, in your gist Erik,
you are considering the index of the touch in the touch list as it's
identifier. Tell me if I'm wrong, but I think it's not necessarily the
case. Especially in cases where some touch ends before ones with higher
ids. So in my version, I'm reading the identifier property of each touch.

------------

As a result from your examples, I'm going to do the following:

decodeTouchList : Decoder (Dict Int Touch.Coordinates)
decodeTouchList =
Decode.field "length" Decode.int
|> Decode.andThen decodeTouches


decodeTouches : Int -> Decoder (Dict Int Touch.Coordinates)
decodeTouches nbTouches =
List.range 0 (nbTouches - 1)
|> List.map (decodeTouch >> Decode.map touchToTuple)
|> Private.Decode.all
|> Decode.map Dict.fromList


decodeTouch : Int -> Decoder Touch
decodeTouch n =
Decode.field (toString n) Private.Touch.decode


touchToTuple : Touch -> ( Int, Touch.Coordinates )
touchToTuple touch =
( touch.identifier, touch.coordinates )

Where `Private.Decode.all` and `Private.Touch.decode` are defined as follow:

-- Private.Decode module

all : List (Decoder a) -> Decoder (List a)
all =
List.foldr (Decode.map2 (::)) (Decode.succeed [])


-- Private.Touch module

type alias Touch =
{ identifier : Int
, coordinates : Touch.Coordinates
}


decode : Decoder Touch
decode =
Decode.map2 Touch
(Decode.field "identifier" Decode.int)
(Decode.map2 Touch.Coordinates
(Decode.field "clientX" Decode.float)
(Decode.field "clientY" Decode.float)
)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Erik Lott
2017-05-08 15:36:13 UTC
Permalink
However, in your gist Erik, you are considering the index of the touch in
the touch list as it's identifier.
Yeah, I think you spotted my bad logic. You're talking about functions like
this, right?

changedTouch : Int -> Json.Decoder a -> Json.Decoder a
changedTouch idx =
Json.at [ "changedTouches", toString idx ]


You're right - All of my functions that decode a single touch are wrong...
and I should likely return a Maybe here as well. Good catch Matthieu.
What about something like this?
Max, regarding the "case" solution, it was actually something I wanted to
avoid. It's definitively working, and I don't think handling more than 8
touch points is either worth it or even correctly managed by hardwares.
It's just that I don't feel good when seeing hardcoded case handling ^^.
TLDR for here: you can easily capture a Json.Decode.Value to use as part
of a decoder, as long as you take your info from it in the same stack, and
from there, you can convert code that uses map, andThen and Ok as results
to Json.Decode.Decoder.
Thanks a lot Art for this write up. I never really thought of the
`decodeValue` trick to manage such a case. It's a handy tool to keep in
mind! I also didn't know about the "same stack" thing!
https://gist.github.com/eriklott/bb78ae787d94e0157befc480fbd6b06e#file-touch-elm
Using `Json.Decode.map2 (::)` as the accumulator function of a `fold` to
build a new `Decoder (List a)` is pretty neat! However, in your gist Erik,
you are considering the index of the touch in the touch list as it's
identifier. Tell me if I'm wrong, but I think it's not necessarily the
case. Especially in cases where some touch ends before ones with higher
ids. So in my version, I'm reading the identifier property of each touch.
------------
decodeTouchList : Decoder (Dict Int Touch.Coordinates)
decodeTouchList =
Decode.field "length" Decode.int
|> Decode.andThen decodeTouches
decodeTouches : Int -> Decoder (Dict Int Touch.Coordinates)
decodeTouches nbTouches =
List.range 0 (nbTouches - 1)
|> List.map (decodeTouch >> Decode.map touchToTuple)
|> Private.Decode.all
|> Decode.map Dict.fromList
decodeTouch : Int -> Decoder Touch
decodeTouch n =
Decode.field (toString n) Private.Touch.decode
touchToTuple : Touch -> ( Int, Touch.Coordinates )
touchToTuple touch =
( touch.identifier, touch.coordinates )
-- Private.Decode module
all : List (Decoder a) -> Decoder (List a)
all =
List.foldr (Decode.map2 (::)) (Decode.succeed [])
-- Private.Touch module
type alias Touch =
{ identifier : Int
, coordinates : Touch.Coordinates
}
decode : Decoder Touch
decode =
Decode.map2 Touch
(Decode.field "identifier" Decode.int)
(Decode.map2 Touch.Coordinates
(Decode.field "clientX" Decode.float)
(Decode.field "clientY" Decode.float)
)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Erik Lott
2017-05-08 16:02:25 UTC
Permalink
updated gist:
https://gist.github.com/eriklott/bb78ae787d94e0157befc480fbd6b06e
However, in your gist Erik, you are considering the index of the touch in
the touch list as it's identifier.
Yeah, I think you spotted my bad logic. You're talking about functions
like this, right?
changedTouch : Int -> Json.Decoder a -> Json.Decoder a
changedTouch idx =
Json.at [ "changedTouches", toString idx ]
You're right - All of my functions that decode a single touch are wrong...
and I should likely return a Maybe here as well. Good catch Matthieu.
What about something like this?
Max, regarding the "case" solution, it was actually something I wanted
to avoid. It's definitively working, and I don't think handling more than 8
touch points is either worth it or even correctly managed by hardwares.
It's just that I don't feel good when seeing hardcoded case handling ^^.
TLDR for here: you can easily capture a Json.Decode.Value to use as part
of a decoder, as long as you take your info from it in the same stack, and
from there, you can convert code that uses map, andThen and Ok as results
to Json.Decode.Decoder.
Thanks a lot Art for this write up. I never really thought of the
`decodeValue` trick to manage such a case. It's a handy tool to keep in
mind! I also didn't know about the "same stack" thing!
https://gist.github.com/eriklott/bb78ae787d94e0157befc480fbd6b06e#file-touch-elm
Using `Json.Decode.map2 (::)` as the accumulator function of a `fold` to
build a new `Decoder (List a)` is pretty neat! However, in your gist Erik,
you are considering the index of the touch in the touch list as it's
identifier. Tell me if I'm wrong, but I think it's not necessarily the
case. Especially in cases where some touch ends before ones with higher
ids. So in my version, I'm reading the identifier property of each touch.
------------
decodeTouchList : Decoder (Dict Int Touch.Coordinates)
decodeTouchList =
Decode.field "length" Decode.int
|> Decode.andThen decodeTouches
decodeTouches : Int -> Decoder (Dict Int Touch.Coordinates)
decodeTouches nbTouches =
List.range 0 (nbTouches - 1)
|> List.map (decodeTouch >> Decode.map touchToTuple)
|> Private.Decode.all
|> Decode.map Dict.fromList
decodeTouch : Int -> Decoder Touch
decodeTouch n =
Decode.field (toString n) Private.Touch.decode
touchToTuple : Touch -> ( Int, Touch.Coordinates )
touchToTuple touch =
( touch.identifier, touch.coordinates )
-- Private.Decode module
all : List (Decoder a) -> Decoder (List a)
all =
List.foldr (Decode.map2 (::)) (Decode.succeed [])
-- Private.Touch module
type alias Touch =
{ identifier : Int
, coordinates : Touch.Coordinates
}
decode : Decoder Touch
decode =
Decode.map2 Touch
(Decode.field "identifier" Decode.int)
(Decode.map2 Touch.Coordinates
(Decode.field "clientX" Decode.float)
(Decode.field "clientY" Decode.float)
)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Matthieu Pizenberg
2017-05-09 00:54:08 UTC
Permalink
Post by Erik Lott
Yeah, I think you spotted my bad logic. You're talking about functions
like this, right?
Yep that was it :)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Loading...