Porting a Module to Elm 0.17

Elm 0.17 has been out for a little while now; it has a slightly different way of doing things compared to 0.16 and before. Like many others, I started porting my existing applications to 0.17 - first I started with my only "production" application, elm typing tutor, which was pretty trivial to update. After that, I moved onto the sample code I wrote for a talk I gave here in Chicago about Elm, which was also pretty simple. Last weekend, I took it upon myself to update my last remaining body of 0.16 code - the first application I wrote in Elm, one I've been keeping up-to-date since 0.14. I haven't released this code publicly yet; but suffice to say it's an application with a coordinate grid that you can click and drag to scroll around. The drag event functionality is encapsulated in its own module, and porting it to 0.17 was a little more challenging than before, so I thought I'd talk about how that went!

Drag.elm - 0.16 and earlier

First things first - here's what Drag.elm looks like as of Elm 0.16:

module Drag where import Mouse import Signal type alias MouseState = { isDown : Bool, wasDown : Bool, prevPosition : (Int, Int), currPosition : (Int, Int) } type alias DragDistance = (Int, Int) mouseState : Signal MouseState mouseState = Signal.foldp (\(isDown, newPos) oldState -> { isDown = isDown, currPosition = newPos, wasDown = oldState.isDown, prevPosition = oldState.currPosition }) { isDown = False, wasDown = False, prevPosition = (0, 0), currPosition = (0, 0) } <| Signal.map2 (\a b -> (a, b)) Mouse.isDown Mouse.position mapDragState : MouseState -> DragDistance mapDragState ms = if ms.isDown && ms.wasDown then let (currX, currY) = ms.currPosition (oldX, oldY) = ms.prevPosition in (currX - oldX, currY - oldY) else (0, 0) drag : Signal DragDistance drag = Signal.map mapDragState mouseState

It's not a lot of code, and it's pretty easy to follow, I think. My favorite part about how this module works in 0.16 is that it exposes two things you need to care about - DragDistance , which is the distance the mouse has been dragged since the previous event, and drag , a stream of drag events. It's extremely simple to integrate into your code; just consume the drag signal and handle it in your update function.

Updating to 0.17 - What's Changed?

Even before 0.17, most modules like Drag.elm resemble the Elm architecture: you have initialization, events you care about, and updating your state according to events. Applications have a view, which isn't really needed for modules.

With 0.17, modules like this one resemble the Elm architecture even more, because now things like Signal.foldp don't exist. It's up to the application developer to call the module's initialization, update, and subscription routines. Another change is that Signal is gone, replaced by Sub and Cmd , which correspond to the types of the events you're interested in, and the events themselves, respectively.

Cmd and Conquer

For this module, I decided to expose an additional event type in addition to the internal events that drag needs to update its state; I wanted the consuming application to get a Drag event whenever a drag occurred. To do that, I needed the module's update function to create a Cmd . There's no function in Elm 0.17 to simply create a Cmd , so this was my first stumbling block. Fortunately, the Elm Slack channel is always helpful, and szabba provided me with a snippet of code that did just that: Task.perform (always <| Drag (dx, dy)) (always <| Drag (dx, dy)) Time.now

The Finished Product

Here's what the finished module looks like, with module documentation omitted for brevity:

module Drag exposing (Model, Msg, initialModel, subscriptions, update) import Task import Time import Mouse type alias Model = { isDown : Bool, currPosition : (Int, Int) } type Msg = MouseUp Mouse.Position | MouseDown Mouse.Position | MouseMove Mouse.Position initialModel : Model initialModel = { isDown = False, currPosition = (0, 0) } subscriptions : (Msg -> msg) -> Model -> Sub msg subscriptions constructor model = let ups = Mouse.ups <| constructor << MouseUp downs = Mouse.downs <| constructor << MouseDown moves = Mouse.moves <| constructor << MouseMove subs = if model.isDown then [ ups, downs, moves ] else [ downs ] in Sub.batch subs dragCmd : ((Int, Int) -> msg) -> (Int, Int) -> (Int, Int) -> Cmd msg dragCmd constructor (px, py) (cx, cy) = let dx = px - cx dy = py - cy task = always <| constructor (dx, dy) in Task.perform task task Time.now update : ((Int, Int) -> msg) -> Msg -> Model -> (Model, Cmd msg) update constructor msg model = case msg of MouseUp _ -> ({ model | isDown = False }, Cmd.none) MouseDown {x, y} -> ({ isDown = True, currPosition = (x, y) }, Cmd.none) MouseMove {x, y} -> let newModel = { model | currPosition = (x, y) } cmd = if model.isDown then dragCmd constructor model.currPosition (x, y) else Cmd.none in (newModel, cmd)

And here's some example code that uses it:

import Html.App as App import Html exposing (Html, text) import Drag type alias Model = { dragModel : Drag.Model, dragDistance : Int } type Msg = DragMsg Drag.Msg | Drag (Int, Int) init : (Model, Cmd Msg) init = let initialModel = { dragModel = Drag.initialModel, dragDistance = 0 } in (initialModel, Cmd.none) subscriptions : Model -> Sub Msg subscriptions model = Drag.subscriptions DragMsg model.dragModel update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of DragMsg msg -> let (newDragModel, dragCmd) = Drag.update Drag msg model.dragModel in ({model | dragModel = newDragModel}, dragCmd) Drag (dx, dy) -> ({ model | dragDistance = model.dragDistance + (abs dx) + (abs dy) }, Cmd.none) view : Model -> Html Msg view model = text <| toString model main : Program Never main = App.program { init = init, update = update, subscriptions = subscriptions, view = view }

You may have noticed that I highlighted a few lines in Main.elm ; I wanted to talk about what I feel is a weakness of 0.17 compared to previous versions of Elm. The highlighted lines all have something in common: they are all spots in the main application code that need to concern themselves with how the dragging module works. Before, Drag.elm took care of its own intialization, event subscriptions, and updating; but now, that responsibility falls to the user.

UPDATE - 2016-06-15

Ahri on Reddit asked me to demonstrate what the code for Main.elm would look like in 0.16, so here it is:

import Drag import Html exposing (Html, text) initialState : Int initialState = 0 view : Int -> Html view dragDistance = text <| toString dragDistance update : (Int, Int) -> Int -> Int update (dx, dy) dragDistance = dragDistance + (abs dx) + (abs dy) main : Signal Html main = Signal.map view <| Signal.foldp update initialState Drag.drag

END UPDATE

Elm 0.17 is great for lowering the barrier to entry to the language, but I'm afraid that it may make things harder on authors of modules like this one, and I'm afraid of the increased boilerplate that consuming these modules requires. "Official" modules like Random don't need this; you don't need to help them set themselves up or manage their state. A quick peek at the code reveals something called an effect module, but I'm guessing that's an internal concept that the core team isn't quite ready for the world to see.

I know that 0.17 and the ideas introduced with it are still very fresh; I'm confident that the core Elm team will come up with ways for authors to write their own event modules in a way similar to Random , keeping Elm easy and fun for module and application authors alike!

Please enable JavaScript to view the comments powered by Disqus.