In which I get angry because some aspect of Elm seems weird, and the docs aren't helping, so I jot down these notes, because writing forces me to think deeply about things in a way that I'm incapable of doing otherwise gaaasspp
Building markup in Elm entails building a data structure, much like React's virtual DOM. That is, you're not building DOM nodes directly, but rather a lightweight data structure that tells the Elm runtime which DOM nodes you want to exist and it figures it out from there. There's really nothing special about Elm's Html
; it's just another data type.
Html
is a parameterized type in Elm. Another such is List
. Thus, you'll be referred to List a
and Html msg
in the docs. The parameters, a
and msg
, stand in for concrete types which you declare in your program, such as List Coord
and Html Msg
.
It's tempting to read List Coord
in English as a list which contains coordinates. This isn't wrong, but it can be misleading. A technically correct description is to replace "which contains" with "associated with", as in, a list associated with coordinates.
That's because a type might not actually contain the things it's associated with. The association can mean that, as in the case of List
, but it might mean something else entirely, as is the case of Html
.
Why belabor the distinction? If we supposed instances of Html
somehow contained instances of messages, that's confusing since it would violate time. HTML can't know in advance what messages it will send. Rather, HTML produces messages, which makes more sense because it doesn't violate time. List Coord
is a contains association, and Html Msg
is a produces association. The meaning of the association is fleshed out in the actual implementation.
Given Html
is a parameterized type, is it possible to have HTML produce integers, instead of messages? Yes, but it wouldn't be as useful. Let's examine why by seeing what would happen if we hack the Elm architecture tutorial's "button" example, replacing Msg
with Int
.
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model = Int
model : Model
model =
0
-- UPDATE
-- not going to use this
-- type Msg = Increment | Decrement
-- hack our update function to use Ints
-- update : Msg -> Model -> Model
update : Int -> Model -> Model
update n model =
case n of
0 ->
model - 1
1 ->
model + 1
_ ->
model
-- VIEW
-- hack our view function to create Html Int
-- view : Model -> Html Msg
view : Model -> Html Int
view model =
div []
[ button [ onClick 0 ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick 1 ] [ text "+" ]
]
If you run this, it actually works, but it should be obvious that we've shoe-horned Int
into a role it isn't well-suited for, especially considering an Int
value can't carry a payload ("tag a value" in Elm-speak) like a union type can.
As you know, HTML elements receive attributes and children. That is, a list of Attribute
instances and a list of Html
instances.
Like Html
, Attribute
in Elm is a parameterized type, as in Attribute msg
. This is mildly confusing, since some attributes don't produces messages. What would it mean for an id
attribute to produce a message? That would be crazy. Just comfort yourself in the knowledge that it never will, and continue reading...
An event handler in Elm is just another kind of attribute which has the honorable distinction of being something that actually does produce messages. In that sense it should probably be renamed event producer. But whatever. Thus, when you create an event handler, you're simply creating an attribute for an HTML (or SVG) element, along with instructions for what message it produces.
The Elm package docs for Html.Events.onClick
describe it thus:
onClick : msg -> Attribute msg
In English, this means, create an onClick attribute by giving it the message you want sent when the element is clicked.
This worries me, because it seems to violate time. In other words, I'm forced to know at render time which message I want sent at click time, but render happens before click. Ack!
That in turn reveals things about my expectations. I expect there will be information available at click time that isn't available at render time, which would influence my decision about what message to send. Is this a valid assumption? No and yes.
No, because it assumes the state of the world would be different at click time than at render time. Elm is an immutable language with pure functions and one-way data-flow, therefore the state of the world and the state of my Elm model are the same, for all I'm concerned about. If and when that state changes, my program re-renders. That's what it means for the application view to be a pure fuction of state.
Now it certainly might be the case that I'm failing to reflect some aspect of world-state in my model. For example, I might want to send message A
if the viewport is below a certain width, and B
otherwise. But all that means is I need to bring the viewport width into my model, so it can inform the view rendering. This would likely be done using some combination of init
and subscriptions
.
The above is a lie! Some aspects of world-state simply aren't accessible to Elm until click time; namely information contained in JavaScript's event
objects. What if I want to act conditionally based on event.clientX
or event.keyCode
for example? It seems my time-violation worries are valid after all!
Fortunately, Elm has a solution for this, by providing custom event handlers which accept JSON decoders instead of messages.
Don't worry, this confused me too. What does JSON have to do with events? I wrote a whole other thing to address that confusion, but suffice to say Elm's JSON decoders are neither JSON nor decoders. Rather, they're translators which take arbitrary JavaScript values and produce Elm ones. For example, it's possible to construct a JSON decoder which takes an event
object and produces a Msg
value.
Crafting such a decoder is beyond the scope of this writing, but suppose we have one called eventToMessage
. All we need to do is pass it to Elm's custom event handler on
.
button [on "click" eventToMessage] [text "click me"]