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 nowelm-lang/html
evancz/elm-svg
is nowelm-lang/svg
evancz/virtual-dom
is nowelm-lang/virtual-dom
evancz/start-app
moved toelm-lang/html
inHtml.App
evancz/elm-effects
moved toelm-lang/core
inPlatform.*
Graphics.*
moved toevancz/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
becomesMsg
inputs
becomesubscriptions
StartApp
becomesHtml.App
Signal.forwardTo
is partially replaced byHtml.App.map
Effects
becomeCmd
- the type of the main function becomes
Program flags
(it used to be some variation onSignal Html
)
Stuff that changed types
port
s that are used for interop have a type like(List String -> msg) -> Sub msg
, where they return aSub
(scription)update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
Stuff that was added
- Effects Managers
- Process
- support for web APIs (websockets, etc)
- a scheduler
- new runtime (see internals section below)
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
(nowMsg
)main
functioninit
values or functioninputs
(now subscriptions)view
functionupdate
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:
- the announcement
- the upgrade guide
- the new official guide to Elm (still missing pieces)
- Mouse drag example
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 Signal
s and Foldp
s, 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.
Comments? Give me a shout at @lambda_cat.
To get the latest post updates subscribe to the LambdaCat newsletter.
You can support my writing on LambdaCat's Patreon.