A few months ago on the Pusher blog I wrote about Making Elm Realtime by using Elm’s port system to integrate an Elm application with messages from PusherJS. When I published that post, the latest Elm version was 0.16 and integrating with Pusher wasn’t as easy as I would have liked it to be.

In May Elm 0.17 was released, and one of the biggest changes was the removal of Signal from Elm, and replacing it with Commands and Subscriptions. We suddenly lost one of the most complex features of Elm, and they were replaced with a system that’s much easier to get your head around. It’s great to now be able to work with the higher level concepts of commands and subscriptions. If Elm’s signals put you off, I encourage you to try Elm again.

In this post I’ll take the code base from my previous Elm post and upgrade it to Elm 0.17, noting the biggest changes. If you want to jump ahead and see all the changes, you can see the pull request I made on the repository that has all the changes. The great news is that Elm 0.17 includes less code to achieve the same functionality, and it’s definitely much reduced in complexity.

Installing the new versions of Elm packages

The first change was to update my elm-package.json to reflect all the new version numbers:

"dependencies": {
    "elm-lang/core": "4.0.0 <= v < 5.0.0",
    "elm-lang/html": "1.0.0 <= v < 2.0.0",
    "evancz/elm-http": "3.0.1 <= v < 4.0.0"
},
"elm-version": "0.17.0 <= v < 0.18.0"

I then removed the elm-stuff folder (not required, but I like to ensure I’m starting from a clean slate when I bump versions!) and ran elm package install to get everything installed and up to date. At this point I got a tonne of errors from the Elm compiler. The great thing about Elm is that its compiler is so great; it’s easy to work your way through errors.

New Conventions and Types

One of the easiest changes is a naming one. Previously we used Action to denote all the events that can happen and how our app reacts to them. Now in Elm 0.17 it’s recommended to use Msg, as things are referred to as messages that flow through a system. This is an easy find and replace!

Next up the signature of the view function has changed. Previously it took a Signal with an address to send actions to. Now that’s all handled for us, and the type has become view: Model => Html Msg. You can read that as “a function that takes a model and returns HTML that can generate messages of type Msg“. This dramatically simplifies all your view logic; there’s no need to pass round address now. Event handlers also are simpler, you just give them a Msg to produce:

-- BEFORE:
button [ onClick address SendMessage ] [...]

-- AFTER:
button [ onClick SendMessage ] [...]

HTML App and Effects

The elm-effects library which we used last time to manage our asynchronous events (such as HTTP requests) has been moved into the core, and exists in two parts, commands and subscriptions. We have two new corresponding types: Cmd and Sub.

A Cmd is something we can give to Elm that will run in the background, and produce a message. Typically you’ll use Cmd Msg to declare commands that will run and produce messages of type Msg.

You can use Sub to create subscriptions, which is useful when you need to be notified when something happens or some data changes. We’ll use these shortly in order to subscribe to some data that’s sent from PusherJS to Elm through a port.

Now we’re using Cmd instead of effects, our update function’s signature also changes:

update : Msg -> Model -> ( Model, Cmd Msg)

Our update function takes the latest message and model and is expected to return a tuple of the new model and any commands that need to run in the background. If you don’t have any commands, you can use Cmd.none to signify that. For example, when we get a NewMessage, we prepend the string and return Cmd.none because there’s no background work to do.

case action of
    NewMessage string ->
        ( { model | messages = string :: model.messages }
        , Cmd.none
        )
    ...

However, when we receive SendMessage, we return the result of postJson model.field, where postJson (which we will define shortly) is a function that takes in a string (the text typed by the user) and returns a Cmd Msg for Elm to run:

case action of
    ...
    SendMessage ->
        ( { model | field = "" }, postJson model.field )

To hook all this together we can use the Elm Architecture. This used to be provided as the Elm Starter App, but now it’s been moved into the Elm HTML package. We can import Html.App as App and use the App.program function to create our app:

main =
    App.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

Along with view and update, which we covered above, there are two other functions to define. init is expected to return ( Model, Cmd Msg ), which is the initial state of our application:

initialModel =
    { messages = [ "Hello World" ], field = "" }


init : ( Model, Cmd Msg )
init =
    ( initialModel, Cmd.none )

And subscriptions has to return a list of subscriptions that we want to listen to (such as mouse movements, keyboard events, or data from ports). This leads us nicely onto talking about the changes to Elm ports in 0.17!

Ports in Elm 0.17

There’s been a few changes to ports in Elm 0.17. The first is that you must now explicitly declare any module that contains ports by updating the module declaration, by prepending port to it:

port module PusherApp exposing (..)

Next, ports now have a different type in Elm 0.17. Our newMessage port now looks like so:

port newMessage : (String -> msg) -> Sub msg

An Elm port takes a function that can turn the data sent over the port (in our case a string, containing the text of the message) and turn it into a message that can be subscribed to. We use lowercase msg here to indicate that the type can be any type and that whatever type the function produces will create a subscription of that type. You don’t need to worry much about this – it’s an implementation detail in how Elm deals with ports.

Once we have our port we can define subscriptions, which takes in a model and returns a subscription:

port newMessage : (String -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model =
    newMessage NewMessage

We call the newMessage port, giving it the NewMessage type, which will take all strings sent through the port and produce a Msg of type NewMessage String that our application can handle. When these are sent, our update function will be called and we’ll get our new message.

HTTP

The final part of the upgrade puzzle was the HTTP logic for posting the data to the server.

Most of this has remained pretty similar but we now have Task.perform that can take a task, run it, and produce Cmd Msg to send through our system.

To call it we give it three things:

  • a function that can take the error response and produce a Cmd Msg
  • a function that can take the successful response and produce a Cmd Msg
  • a Task to run (most often an HTTP request)

Our case is a bit of a special one; we don’t really care about the error or success message (ah, the beauty of building a demo app!), so in my call to Task.perform I just map both the error and success to create a NoOp message. The task itself is provided by httpPostMessage, a function I’ve defined that makes the post request to our server.

postJson : String -> Cmd Msg
postJson str =
    Task.perform (always NoOp) (always NoOp) (httpPostMessage str)

We can call this function from update to kick off the async request in the background:

update : Msg -> Model -> ( Model, Cmd Msg )
update action model =
    case action of
        ...
        SendMessage ->
            ( { model | field = "" }, postJson model.field )

And with that we’re fully up and running with Elm 0.17!

Conclusion

Elm 0.17 greatly reduces complexity and makes it easier to work with the language. Dealing with async activitites, interopability with ports and user interaction is all vastly improved. If you wrote off Elm previously due to the complexities in its Signals, I highly recommend checking it out again. The best place to start is with the Elm 0.17 Guide, or you could check out my Elm talk from PolyConf. If you’d like to see the changes I discussed in this post, you can find the pull request on GitHub.