Migrating from Elm 0.16 to 0.17 - from StartApp

Today the new version of Elm, Elm 0.17 has launched, 6 months after 0.16.

Much has changed, but if you were using The Elm Architecture with StartApp (as it has been recommended for some time) you'll find that migrating won't be troublesome.

If, like me, you were using some fancier tricks, such as having your own Signals, forwarding them, sending them around and maybe even having your own Foldp... well, the plot thickens a bit.

Because Signals, Addresses, Mailboxes and Foldp are gone from 0.17, together with the definition of Elm as FRP.

Before you panic, ask in the Slack for ad-hoc advice for your particular case.

There are new features that might help porting your less conventional structure to Elm 0.17, but it won't be a direct translation.

In a future article, when the dust settles a bit and the docs have some examples, I'll write about how to port code that used Signals, etc.

For now let's look at the simple case.

Libraries that moved around

You'll have to change your elm-package.elm first thing, a bunch of libraries moved to core, and another bunch moved out of core.

First update Elm version:

"elm-version": "0.17.0 <= v < 0.18.0"

Then change the libraries names and versions appropriately:

  • evancz/elm-html is now elm-lang/html
  • evancz/elm-svg is now elm-lang/svg
  • evancz/virtual-dom is now elm-lang/virtual-dom
  • evancz/start-app moved to elm-lang/html in Html.App
  • evancz/elm-effects moved to elm-lang/core in Platform.*
  • Graphics.* moved to evancz/elm-graphics

for example:

"evancz/elm-html": "4.0.2 <= v < 5.0.0"

is now:

"elm-lang/html": "1.0.0 <= v < 2.0.0"

If no matter what, elm-package install always tells you it can't find a way to install the dependencies try deleting ~/.elm.

Stuff that changed name
  • Action becomes Msg
  • inputs become subscriptions
  • StartApp becomes Html.App
  • Signal.forwardTo is partially replaced by Html.App.map
  • Effects become Cmd
  • the type of the main function becomes Program flags (it used to be some variation on Signal Html)
Stuff that changed types
  • ports that are used for interop have a type like (List String -> msg) -> Sub msg, where they return a Sub(scription)
  • update : Msg -> Model -> (Model, Cmd Msg)
  • view : Model -> Html Msg
Stuff that was added
Putting that in practice

Let's start as we would with StartApp.

It's a simple example, so you can just paste the code in Elm Try, even if you don't have a 0.17 on your machine, here is a gist for convenience.

Your imports now look like:

import Html exposing (..)
import Html.App as Html -- was StartApp
import Time exposing (Time, second)

As usual you need:

  • Action (now Msg)
  • main function
  • init values or function
  • inputs (now subscriptions)
  • view function
  • update function

This is how you wire that together in 0.17, using Html.program:

main =
  Html.program
    { init = init
    , update = update
    , view = view
    , subscriptions = \_ -> Sub.none
    }

Action is now Msg

type Msg =
  Tick Time

init is now (Model, Cmd Msg).

(Was generally (Model, Effects Action)).

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

update is now Msg -> Model -> (Model, Cmd Msg)

(Was Action -> Model -> (Model, Effects Action))

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick time ->
      (time, Cmd.none)

view is now Model -> Html Msg.

(Was Signal.Address Action -> Model -> Html)

view : Model -> Html Msg
view model =
  text (toString model)

Addresses are gone, so you can't arbitrarily forwardTo different addresses in other components, or to ports that interop with Javascript as in < 0.16.

So we have the scaffolding for a program that shows the time. To make it work we need to add the subscription:

subs : Model -> Sub Msg
subs model =
  Time.every second Tick

and update subscriptions in main:

main =
  Html.program
    { init = init
    , update = update
    , view = view
    , subscriptions = subs
    }

inputs are now subscriptions, the type is Sub.
They work a little bit like signals, as in you can merge them, and you "subscribe" to them, getting new events as they appear, but they haven't got any addresses.

And that's it, this program compiles and runs, shows you a dubiously useful float that represents the time. But still, it runs and gets you used to a few new concepts.

One last thing, let's add a button that sends a pointless Msg, just to try out how it works now.

So let's change our imports:

import Html exposing (..)
import Html.App as Html
import Time exposing (Time, second)
import Html.Events exposing (..)

then add to Msg:

type Msg
  = Tick Time
  | Clicked

Then add the sending of the Msg view:

view model =
  div [] [text (toString model)
         , button [onClick Clicked] [text "button"]
         ]

As you can see sending Msg is now onClick <Msg> instead of onClick address <Msg>.

Then handling it in update:

update msg model =
  case msg of
    Tick time ->
      (time, Cmd.none)
    Clicked ->
      (0, Cmd.none)

everytime you click the button, the time will show 0, and then when it gets a new Time Sub update it shows the newest time again.

Hopefully that's enough to get you started porting your StartApp apps to 0.17.

Official Docs

For more info, do read:

Internals

So you may ask, how do things get around without Signals and Mailboxes?

Let's take a look at the internals to find that out, but no guarantees anything will stay like this, ping me if the code has moved around on the elm repo.

Let's start at Html.App to see how that is implemented:

programWithFlags
  : { init : flags -> (model, Cmd msg)
    , update : msg -> model -> (model, Cmd msg)
    , subscriptions : model -> Sub msg
    , view : model -> Html msg
    }
  -> Program flags
programWithFlags =
  VirtualDom.programWithFlags

this is very much like the StartApp.start definition, but around here StartApp we'd see Signals and Foldps, though they're not to be found in this file.

Digging further, let's look at Platform, where Program is defined.

-- MyApp.main : Program { userID : String, token : Int }

[...]

type Program flags = Program

this is not greatly enlightening either, though I do recommend you to read the comments in the files for more info.

We need to jump yet again, this time to Platform.js, where at last we find the function that translates the main function to an actual VirtualDom$programWithFlags:

function mainToProgram(moduleName, wrappedMain)
{
	var main = wrappedMain.main;

	if (typeof main.init === 'undefined')
	{
		var emptyBag = batch(_elm_lang$core$Native_List.Nil);
		var noChange = _elm_lang$core$Native_Utils.Tuple2(
			_elm_lang$core$Native_Utils.Tuple0,
			emptyBag
		);

		return _elm_lang$virtual_dom$VirtualDom$programWithFlags({
			init: function() { return noChange; },
			view: function() { return main; },
			update: F2(function() { return noChange; }),
			subscriptions: function () { return emptyBag; }
		});
	}

	var flags = wrappedMain.flags;
	var init = flags
		? initWithFlags(moduleName, main.init, flags)
		: initWithoutFlags(moduleName, main.init);

	return _elm_lang$virtual_dom$VirtualDom$programWithFlags({
		init: init,
		view: main.view,
		update: main.update,
		subscriptions: main.subscriptions,
	});
}

...as you can see we're not in Elm-land any more.

I expect/hope that in the next article I'll cover how to talk to sub Components in a nested structure, and maybe how to port structures that used signals extensively.