It has been few month since I’ve started to learn Elm programming language. I’ve been walking around elm for quite a while without ever touching it. After a while I decided to learn some statically typed functional language. This was a time when I finally decided to give elm a try. So I started to play with it. Learn a little bit of learning syntax, core functions, types, Signals, Effects, Mailboxes and Elm architecture. It was quite interesting journey. Soon I’ll become wonder if I’ll be able to write some smaller project from end to start just using elm. Some kind of real world application to challenge myself and also this world of typed purity. I wanted it to talk to some JSON API. Github’s API seems to be good think to start with so I start writing github repository browser in elm. This is demo of app I’m talking about.

Just few days ago to me unexpected thing happens. Elm 0.17 release was announced! Everyone who read that article was like “Omg, what’s going on?!”. From that moment Signals, Messages, Effects and Mailboxes are past of Elm. What should I think of it?

This was really huge change in how everybody (including me) things about building web apps in elm. We all know these kind of changes are really hard to bring in practice when you already have large codebase around your ecosystem. I that moment that app I wrote few month ago came to my mind. It looks like all packages it needs are ready for 0.17. Plan was made and yesterday I finally found some time to upgrade it to 0.17.

Upgrading is always painful. It really is. I’m working on pretty large Ember.js application in my company. Ember core team is pretty careful about their deprecation planing. Anyway upgrading that app was (and is) a huge pain. It’s always a huge pain when you have tens of thousands of code lines written with large test suites.

Such a big design change that elm 0.17 brings to its ecosystem is something that can be impossible to happen to Javascript framework. Remember Angular 1.x vs 2.x? Express.js vs Koa.js?

Luckily I have pretty small app written in elm which seems to be good candidate for going through this big upgrade process. I was not afraid of upgrading this since I was able to wrote whole app in one day or so. Even if I will need to rewrite it completely it will be still just a weekend project.

One thing I think is particularly hard when starting with any upgrade is to find some pattern and split work into few iterations where each has its own goal. You can’t rewrite whole app at once. This is no how you design it and it shouldn’t be the way how you refactor it as well.

To be honest I was quite chaotic when I started with this upgrade but soon I realised some (or way how you can split upgrade into smaller iterations if you want) and this is why I’m sharing this story with you. No matter how big your project is I think you can apply these steps in order to make whole upgrade process of your elm 0.16 to 0.17 sooth and fun.

Note: This article is just about upgrading web app based on elm-html. I also have one project written with canvas backend (it’s kind of game) and other one as a library backed up just by elm-test. Anyway I will not focus on nether of these projects.

Lets get started

You can check or clone whole 0.16 version code from Github under v0.16 tag. With our legacy code and Upgrading to 0.17 guide opened we are ready to try what new Elm is about.

New dependencies

This is pretty straight forward. We can almost copy/paste this part from upgrade guide so Lets open elm-package.json in your favorite Emacs and get it done.

Note: As you can see I’m a little bit sloppy. Almost all meta is not filled out but since this was never published (and never will be) as package we just leave it as it is for now. One more thing to mention is that there is elm-test specified as dependency in legacy version but I actually never used it in this project. This is why it’s not in updated version

Then we run package install as usual:

$ elm-package install

Module declaration

With new packages installed we can start rewriting application logic for new APIs. One smaller change is to update model declaration to new syntax. Replace:

module Repos where

with:

module Repos exposing (main)

We can also change imports to match new module structure.

Here is complete old version and new equivalent:

As you can see this app needs much less modules using 0.17.

Views + StartApp

Next step according to upgrade guide should be replacing Action with Msg. Anyway I’d like to start with views since my first goal is to be able to render initial state of application. We will go back to update function in a minute.

What I did is to get rid off address passing from view, change type annotations and comment out all events. This is basically what needs to be done:

replace:

view : Signal.Address Action -> Model -> Html

view address model =

with:

view : Model -> Html Msg

view model =

replace all calls to “sub views”

div [ class “app-container” ]

[ headerView address model ]

to:

div [ class “app-container” ]

[ headerView model ]

And finally comment out Events (we get back to them later):

[ div

[ class “repo-main”

, Events.onClick address (SelectRepo repo) ]

to:

[ div

[ class “repo-main”

-- , Events.onClick address (SelectRepo repo)

]

I also had custom Events for input and submit. In this step we can simply comment them out.

-— onInput : Signal.Address Action -> (String -> Action) -> Attribute

—- onInput address f =

—- Events.on “input” Events.targetValue (\v -> Signal.message address (f v)) —- oSubmit address value =

—- Events.onWithOptions “submit”

—- { stopPropagation = True, preventDefault = True }

—- Json.value (\_ -> Signal.message address (FetchData value))

If you now try to compile app you can still see compile error. We no longer need port in 0.17! Let’s remove it then!

Fixing glue

Now we can see much more reasonable errors during compilation. Mostly legacy Effects stuff, missing Msg type and StartApp. Let’s fix this one by one.

Firstly we can comment out all Effects stuff since our first goal is to render initial state. Also we need to get rid off all calls to these functions. For now lets replace every call in update with Cmd.none which is replacement for legacy Effects.none.

Also it looks like we are now in update function part so lets change Action type to new Msg and refactor arguments according to guide.

This is how update action looks like after these changes:

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

update action model =

case action of

NoOp ->

( model, Cmd.none )

FetchData name ->

( { model

| isLoading = True

, resultsFor = model.userName }

, Cmd.none )

—- , fetchDataAsEffects model.userName )

FetchDone results ->

( { model

| repos = results

, isLoading = False

, alert = “” }

, Cmd.none )

Note: This is just simplified version, but you get the idea…

I my case I also need to change initialState since fetchData function is also called in this place. I’ll replace it with Cmd.none too.

init : ( Model, Cmd Msg )

init =

( initialModel

, Cmd.none )

— , fetchDataAsEffects initialModel.userName )

At last but not least we still did not change main function and legacy StartApp code. This can be copy/pasted from upgrade guide. Result looks like this:

main : Program Never

main =

Html.App.program

{ init = init

, update = update

, view = view

, subscriptions = \_ -> Sub.none }

App now renders without errors!

Ok. Looks like there is a lot of going on. This how whole code looks like after these changes.

Goodbye Effects!

So app is kind of working now. They said if it compile, than it works, right? Anyway it’s not much useful since it do nothing. I think it’s good time to have a look at Effects. With Http working we will be able to fetch first data which seems to be reasonable next stage.

Looking to new api documentation I think we will no longer need function for transforming Results to Action. I started by removing this one. Also one for creating Effects Action seems to be useless. Let’s remove this one too.

Let’s have a look at fetchData function. Looks like we will need to change this one a little bit. Here is old version:

fetchData : String -> Task.Task a (Result Http.Error (List Repo))

fetchData name =

Http.get reposDecoder (getUrl name)

|> Task.toResult

new implementation looks like:

fetchData : String -> Cmd Msg

fetchData name =

let url =

getUrl name

in

Task.perform FetchFail FetchDone (Http.get reposDecoder url)

As you can see we will need to change error handling logic. In my case I just added Type FetchFail to Msg union type. This also means we need to add this branch to pattern matching inside update function. Lets replace old Error with new FetchFail. We can reuse old httpErrorToString function for transforming Error to String.

type Msg = NoOp

| FetchData String

| FetchDone (List Repo)

| FetchFail Http.Error -- THIS!!!!!!!!!!!

| NameChanged String

| SelectRepo Repo

| ChangeSort SortBy update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

case msg of

NoOp ->

( model, Cmd.none )

FetchData name ->

( { model

| isLoading = True

, resultsFor = model.userName }

, Cmd.none )

— , fetchDataAsEffects model.userName )

FetchDone results ->

( { model

| repos = results

, isLoading = False

, alert = “” }

, Cmd.none )

FetchFail error -> -- THIS!!!!!!!!!!!!

( { model

| repos = []

, isLoading = False

, alert = (httpErrorToString model.userName error) }

, Cmd.none )

Nice! App should now compile without errors.

Now we can change init function and try if it works. Change:

init : ( Model, Cmd Msg )

init =

( initialModel

, fetchDataAsEffects initialModel.userName )

to:

init : ( Model, Cmd Msg )

init =

( initialModel

, fetchData initialModel.userName )

It works! There is no compile error, request is sent and results are rendered. So far so good!

To complete this part we can just remove Cmd.none and call fetchData in update function’s FetchData branch which will be triggered later via user interactions.

FetchData name ->

( { model

| isLoading = True

, resultsFor = model.userName }

, fetchData model.userName )

This is snapshot of code at this stage:

Events

We are almost finished. Last missing step is to bring back events to allow user interactions.

I’d like to start with uncommenting Events.onClick and removing address argument like this:

[ button

[ class (classNames Name)

— , Events.onClick address (ChangeSort Name) ]

]

[ text “name” ]

new version:

[ button

[ class (classNames Name)

, Events.onClick (ChangeSort Name) ]

[ text “name” ]

Now sorting buttons and repository description should work.

This is cool but we are still missing user name update and form submitting. In forms I’d like to use submit instead click since this makes Enter and other handy UX tweaks for mobile. Do you remember that two functions we previously commented out? One of the did the trick with Submit, other one hooks input event. Let’s have a look at Html.Events documentation.

Looks like new version have these two build in!

This means we can remove both commented functions and give the build-in ones a try.

change:

input

[ value model.userName

— , onInput address NameChanged

, class “search-field” ] []

to:

input

[ value model.userName

, Events.onInput NameChanged ] []

and:

Html.form

— [ onSubmit address model.userName

[ class “search-form” ]

to:

Html.form

[ Events.onSubmit (FetchData model.userName)

, class “search-form” ]

We are (almost) done!

This is final version of our new implementation:

Load app in Html

Last step is to make new version of application work with Html. Just follow upgrading guide. This how mine index.html looks like:

<!DOCTYPE HTML>

<html>

<meta charset=”UTF-8">

<title>Github repository browser</title>

<link href=’

<link href=’

<link rel=”stylesheet” href=”dist/styles/main.css”>

<script type=”text/javascript” src=”dist/Repos.js”></script>

</head> Github repository browser https://fonts.googleapis.com/css?family=Open+Sans' rel=’stylesheet’ type=’text/css’> https://fonts.googleapis.com/css?family=Lato:100italic' rel=’stylesheet’ type=’text/css’> <body>

</body> <script type=”text/javascript”>

var app = Elm.Repos.fullscreen();

</script> </html>

Nothing too fancy. As you can see there is new api for rendering app in fullscreen.

just change:

var app = Elm.fullscreen(Elm.Repos);

to new version:

var app = Elm.Repos.fullscreen();

That’s it. We are done!

Or not? (edited on May 28th)

As jediknight pointed out in discussion on reddit there is actually much nicer way how to create model msg tuple using `!` infix function! I recommend to use it since it adds some extra abstraction to your actions. This function takes two a arguments — model (whatever) and List of Msg. Let’s see how we can use it.

this is on of our old update branches:

update msg model =

case msg of

NoOp ->

( model, Cmd.none )

and this is how it looks like with use of `!` function

update msg model =

case msg of

NoOp ->

model ! []

As always here is shippet of whole application:

Conclusion

I think this demonstrates one possible pattern of how to upgrade your web app written in elm to version 0.17. We started with views. Removing all calls to code we do not need to just get initial render. Then fixing update function and main (StartApp) code. Doing so bring us to state where we were able to get initial render. Then we made Http work so our app was able to fetch data a render them. We made events work so user can interact with app as before. And finally we connected our app to html.

Elm, Architecture, Types…

Somewhere above I was taking about how hard it is to upgrade your application. I also mentioned few examples where the difference between legacy and new API made it almost impossible to upgrade existing code bases. I think with elm the situation is different.

This is on of scenarios where types really cover your back. Thanks to the fact that elm is strongly and statically typed it’s not such a big deal to introduce huge API changes as 0.17 did. I’m not a hardcore static type fan boy. I think there are big trade offs while choosing static over dynamic typed languages. Dynamic typing should be pretty powerful in some cases. I’m also pretty big fan of meta programming (think about Lisp(s) or Ruby). Anyway with static typing comes some extra safety. Extra safety in terms that you no need to be afraid start aggressively rewriting parts of your code. I think this is one thing Elm really shines in. My bets on Elm are pretty high thanks to this. I think Elm’s community can really benefits of this nature of safety. I expect there is bright future for Elm as language and architecture. The potential of bringing cutting edge concepts and constantly iterating on all its building blocks can really runs over whole React and Clojure Script word. I strongly believe that neither of these ecosystems will be able to implement experimental primitives rethink its design and more importantly bring all of this easily to large scale production applications as Elm can.

Thanks to everyone in this community for making all of this happened. I’m super excited about what came next.