Notes on Elm
Nov 2, 2016
I recently got interested in the Elm programming language. A little while ago I started reading the PureScript docs and then later started learning Haskell. However I'm generally much more focused on frontend development and could never really see myself using either of those languages for much.
Elm has most of the awesome functional programming features of Haskell. It also has a lot of similarities with Redux and can actually be used to create real-world applications.
Here are some of my rough notes from reading through the Elm docs. I also implemented most of the examples from the docs, with my own modifications, in this repo.
Basic syntax
- Strings require double quotes
- String concatenation with double plus
++
- Rounded integer division supported with double slash
//
(like in Python) - True and False values need to be capitalised (like in Python)
if cond then value1 else value2
(like Python again)- There is no concept of truthy/falsy values (unlike JavaScript)
- List functions:
List.isEmpty
,List.length
,List.reverse
,List.sort
,List.map
- Tuples with brackets for returning multiple values
- Records like objects, define with equals symbols instead of colons
{ x = 3, y = 4 }
, access with dot notationpoint.x
- Special dot notation to create a function to access that property of a record (
.propName record
will be equal torecord.propName
) - Can do destructuring on records
under70 {age} = age < 70
- Can assoc/merge in new properties to an existing record with pipe notation
{ bill | name = "Nye" }
- In Elm (as opposed to JavaScript) you cannot ask for a prop that does not exist and there is no null or undefined
- Elm's records support structural typing
The Elm Architecture
Basically identical to Redux:
- Model: State
- View: React view
- Msg: Actions
- Update: Reducer
The view has type Model -> Html Msg
which means that the generated HTML can output Msg
values which automatically get fed into the update/model cycle for you (when initialising the app with main = Html.beginnerProgram { model = model, view = view, update = update }
)
HTML is basically generated with constructors named after the elements followed by a list of props then a list of children.
Project setup
npm i -g elm
elm package install
elm-reactor
# Then open up http://localhost:8000/ for a pretty neat UI
Playing with the counter example
- It ran fine
- Easily added a button component
- Adding a class name to a component requires
div [ attribute "class" "my-class" ] []
withattribute
from theHtml.Attributes
package
Something I don't understand:
I wrote this function to abstract away the creation of a button style:
buttonStyle : Model -> Attribute
buttonStyle model = style [ ("color", if model == 0 then "red" else "black") ]
And elm-reactor kept complaining with the following:
-- TOO FEW ARGUMENTS ------------------------- Main.elm
Type Html.Attribute has too few arguments.
39| buttonStyle : Model -> Attribute
^^^^^^^^^
Expecting 1, but got 0.
Removing the type works fine and loading it up in the repl tells me the type is the following:
<function> : number -> Html.Attribute a
I don't currently know what the a
argument passed to the Html.Attribute
type is doing...
Edit: After further experimentation, I think that Html.Attribute
accepts one argument (to make it a generic type or whatever that's called in Elm). If that argument starts with a lower-case character then it is a generic type variable. However the actual type that it needs to match with in this case is the Msg
type (because the attributes can potentially be handlers which return messages I guess).
Form example
- Not sure what is going on constructing the model with three empty strings (
model = Model "" "" ""
), putting values in there seems to update the values of the model even though when the HTML renders for the first time, there are no values in the text boxes (not sure if bug or intended behaviour) - Edit: Wait, I just realised that it's because the inputs are never actually being passed a value, they only have an
onInput
handler which won't actually control the initial value passed... - Seems that the "type" HTML input attribute has to be called
type'
to avoid a naming clash - Can use
let ... in ...
syntax for (multiple) temporary variables
Effects
For producing effects, your model should return a tuple of the new model plus an effect command. Use Cmd.none
for an empty command.
- Use
Random.generate
to create a command to generate a random action - Must also tell it how to create a message:
Random.generate NewFace (Random.int 1 6)
Just wrote out the basic dice program from scratch without looking at anything for reference. Seems relatively straightforward. Apparently Sub
and Cmd
appear to be always available and they both have a .none
property to do nothing.
Difference between type
and type alias
:
type alias
is just an alias for something that already would have been a valid type before (e.g. a specific type of record)type
constructs a new type that did not exist before, it defines a union of new type constructors (which accept any arguments) used to construct something of that type
Http fetching seems okay, if a tad verbose:
Task.perform FetchFail FetchSucceed (Http.get decodeGifUrl url)
- Use
Task.perform
for something that might fail with two message constructors for each case - Need to decode the server response JSON using the
Json.Decoder
stuff which I won't write about here - WebSockets seem nice although I've never actually used them on the server before anyway
Modularity
Modules expose stuff at the top of the file:
module Counter exposing (Model, Msg, init, update, view)
In the above example, external code cannot instantiate a value of type Msg
because it only has access to the Msg
type and not the type constructors that make it up.
App.map Top (Counter.view model.topCounter)
- Compose views with
App.map
to make any messages passed from theHtml
get preceded by theTop
constructor
More notes on types
- Function definitions are just shorthand for assigning a lambda expression:
half n = n / 2
is treated identically tohalf = \n -> n / 2
(the syntax is obviously much more useful when you have multiple arguments) - You can have type variables
type List a = Empty | Node a (List a)
- You can have triple quoted strings
decodeString user """{ "name": "Tom", "age": 42 }"""
How to use in the real world
- Generate full index.html file and let it set everything up for you
- Generate a JavaScript file which sets a global
Elm.MyModule
variable to have three methods:fullscreen()
to take over the body tag,embed(node)
to mount on a node andworker()
to just run it with no UI - Could theoretically use with React with the embed method style and React refs to get access to the DOM node to mount on
Ports
- Initialise an app with
const app = Elm.AppName.fullscreen()
- Subscribe to a port with
app.ports.check.subscribe(callback)
- Send data back through a port with
app.ports.suggestions.send(data)
Then create ports with the following syntax:
port check : String -> Cmd msg
port suggestions : (List String -> msg) -> Sub msg
The syntax is a bit weird but the basic idea is that you use the first port and provide it with a string value. This then returns a command and when that command is passed to Elm, it notifies the JavaScript subscriber on that port with that string value.
The second type of port is given a function that can interpret whatever JavaScript gives it and returns a message. After being given a processing function, it returns a subscription which is passed to Elm and will process anything given on that port and return a message which gets dispatched to the update cycle.
Most JSON-like values can be transferred and there are various rules to prevent incorrect things getting in (if you try to pass something wrong from JavaScript it will throw an error before even reaching Elm).