Custom flags decoder in Elm
May 31, 2017 · 5 minute read · Commentselm
I’m preparing a talk about Elm and JS interoperability for Prague Elm Meetup and I got to the topic of flags, i.e. how to initialize an Elm app with data passed in from JavaScript. Looking into it, I realized how much I don’t know and had to find out. There was one special question which bothered me from the moment I tried using flags: Elm automatically decodes the data which limits it to elementary types and their combinations.
Classic Elm application without any inputs will probably use Html.program
to
- seed model with the initial state and commands in
init
, - describe how the model is changed as a response to messages (and if any commands are issued) in
update
, - define
view
as a function of the model, and - subscribe to event streams in
subscriptions
.
This means that the application doesn’t need any data from the outside (e.g. a calculator), all data is fixed and a part of the application (e.g. random text generator with a static vocabulary), or it is willing to show a spinner to the user and fetch all data asynchronously via commands. In most cases, we integrate Elm applications into an existing ecosystem which can provide some bootstrap data immediately.1
To pass data to and Elm application, you have to replace Html.program
by Html.programWithFlags
. This does not affect update
, view
, and subscriptions
but it changes the type of the init
function:
init : (model, Cmd msg)
becomes
init : flags -> (model, Cmd msg)
This means that init
takes an argument of type of your choosing and converts it to the initial model and commands.2
Let’s pass locale code into our application. We’ll need locale
field in our model:
type alias Model =
{ locale : String
, meters : List Meter
-- ...
}
We’ll define our flags type as an alias for String
which represents the locale code:
type alias Flags = String
So we can build the initial model with our flags:
init : Flags -> (Model, Cmd Msg)
init flags =
{ locale = flags
, meters = [] } ! []
The JS side is simple as well:
var flags = "en";
Elm.Main.fullscreen(flags);
Elm will take care of parsing the argument passed to the fullscreen
and converting it to our Flags
type. As mentioned at the top of this article, this process is automatic which is great as long as your flags type does not include any union types because Elm doesn’t know how to decode those and never starts the application.
Let’s see this in action! In Enectiva, we work a lot with meters, so it’s common that an Elm app would need a list of meters and sometimes this list might be passed in as part of flags:
var flags = {
locale: "en",
meters: [
{
id: 1,
name: "Main electricity meter",
energy: "el"
},
{
id: 2,
name: "Main water meter",
energy: "wa"
}
]
};
Elm.Main.fullscreen(flags);
This works great as long as the meter type is defined something like this:
type alias Meter =
{ id : Int
, name : String
, energy : String
}
With flags:
type alias Flags =
{ locale : String
, meters : List Meter
}
Energies are, however, a fixed set of values and it makes sense to define a union type for them:
type Energy = El | Wa | He | Co | Ga
This changes the energy
field in Meter
type to Energy
and suddenly Elm can’t automatically convert the data passed from JS into Flags
type. It will give us a nice error message describing our predicament but it won’t help much.
Our old approach was to define an alternative type for Meter
which was identical except for having string energy
. This would let Elm decode the flags correctly, and we’d then have to go and manually convert the strings into values of Energy
. Definitely not an elegant solution an it would get quite hairy with more complex types.
It would be much easier to give Elm our own decoder to parse the incoming data but such mechanism isn’t there, it would be apparent from the types. A dirty hack would be saying that meters
field in flags is a String
and then parse that in init
with the decoder. But the flags would then need to have a JSON string as a value of a field in a JSON object — far from perfect.
Since I’m preparing a talk, I feel I should give my best to be accurate, to be up-to-date, and to present the optimal approach, not hacks. I tried to google for this problem on several occasions and never found anything useful, so I had to dig into Elm’s source code. My question was: how does Elm derive the decoder for flags?
After running through few modules, I reached runHelp
in the native portion of JSON decoding library. This is the core of the decoding process: it compares the elementary decoder type to the actual JSON value and returns either an Ok
value or an Err
which will eventually halt the decoding process. The key component here is the value
branch which always succeeds with the value. This clicked with the error message for bad flags mentioning Json.Decode.Value
as an acceptable type!
What this means that if you say that any part of the flags type is Json.Decode.Value
, Elm won’t try to parse it further and will make it accessible as such. We can then take this value and pass it to Json.Decode.decodeValue
alongside our own decoder. Exactly our goal all along!
Here’s a full example:
Flags
are a bit mysterious by using Json.Decode.Value
and init
has to do the extra work of parsing it, but the result is perfect and feels very Elm-ish.
We're looking for developers to help us save energy
If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.