Update (2019-11-06): I think what I describe is an example of what people call "parse, don't validate".

I wrote a simple video player in Elm as an experiment. My earliest realization was that authoritative source for player state the HTML media element (<audio> or <video>) and not the Elm application. I decided to keep the Elm model in sync by sending the JSON-encoded HTMLMediaElement over a port whenever any change in the media element occurs.

var events = [
/* ... and many more... */

events.forEach(function(eventName) {
media.addEventListener(eventName, function(event) {

It's tempting to define a model on the receiving side that represents the JSON data structure as is. The fields would be as defined by the specification. It would be easy to look up documentation and trace the flow of values. For example, one could look at the Elm debugger and see that there is a media.readyState value of 4, then look up what a "ready state" is and what HAVE_ENOUGH_DATA (defined as 4) means.

However, I don't like that:

I prefer the following data structure:

{-| Current status of media playback

type Playback
= Playing
| Paused
| Ended
| PlaybackError Error

type Error
= UnsupportedSource
| BrowserPolicyViolation
| NetworkConnectivity

It's missing some states I didn't need for my experiment, like "loading metadata" or "buffering", but it's easy to add them.

What's neat about this type is that the variants are mutually exclusive. It can avoid errors like trying to programmatically pause a video that has already ended. It can also make state machine transitions explicit, like "load a new video when this one ends playing".

handleMediaUpdate : Model -> Media.State -> ( Model, Cmd Msg )
handleMediaUpdate model media =
case media.playback of
Media.Ended ->
playNext model

-- ...

How do we transform the JSON representation of the media element into this handy data structure?

The JSON has this shape:

"autoplay": false,
"controls": false,
"currentTime": 3.999659,
"ended": false,
"error": null,
"paused": true,
"seeking": false,
"src": "http://localhost:8000/test.wav",
"volume": 1
// and many other fields...

To build a Playback value out of this, we need to decode multiple fields, then combine them.

{-| State decoder for HTMLMediaElement objects

decoder : Json.Decode.Decoder Playback
decoder =
Json.Decode.map3 toPlayback
(Json.Decode.field "paused" Json.Decode.bool)
(Json.Decode.field "ended" Json.Decode.bool)
(Json.Decode.field "error" errorDecoder)

The trick is in the toPlayback function:

{-| A helper function to decode related fields into a custom type. Be careful
with the order of arguments, there is potential for confusing the two booleans

toPlayback : Bool -> Bool -> Maybe Error -> Playback
toPlayback paused ended maybeError =
case ( paused, ended, maybeError ) of
( _, _, Just error ) ->
PlaybackError error

( _, True, _ ) ->

( True, _, _ ) ->

_ ->
-- FIXME: it's only really playing if readyState is 3 or 4

(The catch-all branch is an oversimplification. As far as I know, we also need to take into account the readyState to derive whether the media is playing. I took a shortcut in the initial implementation.)

With this, the complexity is handled at the system boundary. It is the only spot that deals with booleans. It's also the only place in the Elm code which creates a Playback value. I would know where to look if there was a bug in the playback state.