Elm 0.17 - Successful Upgrade of Real World App …and some soft of guide to all of this
Note: This is mirror of my original article published on medium.
It has been few month since I’ve started learning Elm programming language. I’ve been walking around elm for quite a while without ever touching it. However not so long ago I’ve decided to learn some statically typed functional language started reading Learn You Haskell for Great Good! and so I also finally put my hands on Elm. I’ve learned bit of syntax, core library, 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 and use Html. 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! I remember myslef reading that announcement and thinking “Omg, what’s going on?!”. From that moment Signals, Messages, Effects and Mailboxes are past of Elm. What should I think of it?
@rabbitonweb I'm not sure yet. It's interesting and scary at the same time.
— Marek Fajkus (@turbo_MaCk) May 11, 2016
This was really huge change in how everybody (including me) thinks about building web apps in Elm. We all know these kind of changes are really hard to bring in practice since you already have large codebase around your ecosystem. In 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 and started with upgrade to 0.17.
Upgrading is always painful. It really is. I’m working on pretty large Ember.js application at 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 complicated when you have tens of thousands of code lines and large test suits already written for legacy API.
Such a big design change that Elm 0.17 brings to its ecosystem is something that can easily kill any JavaScript framework. Remember Angular 1.x vs 2.x? Express.js vs Koa.js?
Luckily I have pretty small app which seems to be good candidate for going through this big upgrade process. I was not afraid of starting with upgrade since I was able to write whole app in just one day or so. Even if it turns out to be complete rewrite it can still be done over weekend.
One thing is particularly hard when starting with any upgrade which is to find some pattern and split work into few smaller iterations where each has its own goal. Mostly 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 upgrad 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.
Old version:
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"deadfoxygrandpa/elm-test": "3.1.0 <= v < 4.0.0",
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-effects": "2.0.1 <= v < 3.0.0",
"evancz/elm-html": "4.0.2 <= v < 5.0.0",
"evancz/elm-http": "3.0.0 <= v < 4.0.0",
"evancz/start-app": "2.0.2 <= v < 3.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}
New version:
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"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"
}
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:
module Repos where
import List
import Graphics.Element exposing (..)
import Http
import Json.Decode as Json exposing ((:=))
import Task
import Signal
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events as Events
import Effects exposing (Effects)
import StartApp
module Repos exposing (main)
import List
import Html exposing (..)
import Html.App
import Html.Attributes exposing (..)
import Html.Events as Events
import Http
import Json.Decode as Json exposing ((:=))
import Task exposing (..)
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:
: Signal.Address Action -> Model -> Html
view = view address model
with:
: Model -> Html Msg
view = view model
replace also all calls to “sub views”
div [ class “app-container” ]
[ headerView address model ]
with something like
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)
]
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:
: Msg -> Model -> (Model, Cmd Msg)
update =
update action model case action of
NoOp ->
( model, Cmd.none )FetchData name ->
( { model| isLoading = True
= model.userName }
, resultsFor
, Cmd.none )- , fetchDataAsEffects model.userName )
—FetchDone results ->
( { model| repos = results
= False
, isLoading = “” }
, 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 ).userName ) — , fetchDataAsEffects initialModel
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:
: Program Never
main =
main
Html.App.programinit = init
{ = update
, update = view
, view = \_ -> Sub.none } , subscriptions
App now renders without errors!
Ok. Looks like there is a lot of going on. This is 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 at the 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:
: String -> Task.Task a (Result Http.Error (List Repo))
fetchData =
fetchData name
Http.get reposDecoder (getUrl name)|> Task.toResult
new implementation looks like:
: String -> Cmd Msg
fetchData =
fetchData name let url =
getUrl namein
FetchFail FetchDone (Http.get reposDecoder url) Task.perform
As you can see we will need to change error handling logic. In my case I just added Type FetchFail to Msg union type.** Thir 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
: Msg -> Model -> (Model, Cmd Msg)
update =
update msg model case msg of
NoOp ->
( model, Cmd.none )FetchData name ->
( { model| isLoading = True
= model.userName }
, resultsFor
, Cmd.none ).userName )
— , fetchDataAsEffects modelFetchDone results ->
( { model| repos = results
= False
, isLoading = “” }
, alert
, Cmd.none )FetchFail error -> -- THIS!!!!!!!!!!!!
( { model| repos = []
= False
, isLoading = (httpErrorToString model.userName error) }
, alert , 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.userName ) , fetchDataAsEffects initialModel
to:
init : ( Model, Cmd Msg )
init =
( initialModel.userName ) , fetchData initialModel
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
= model.userName }
, resultsFor .userName ) , fetchData model
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:
[ buttonclass (classNames Name)
[ ChangeSort Name) ]
— , Events.onClick address (
] [ text “name” ]
new version:
[ buttonclass (classNames Name)
[ ChangeSort Name) ]
, Events.onClick ( [ 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.userName
[ value modelNameChanged
— , onInput address class “search-field” ] [] ,
to:
input.userName
[ value modelNameChanged ] [] , Events.onInput
and:
Html.form.userName
— [ onSubmit address modelclass “search-form” ] [
to:
Html.formFetchData model.userName)
[ Events.onSubmit (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>
<head>
<meta charset=”UTF-8">
<title>Github repository browser</title>
<link href=’https://fonts.googleapis.com/css?family=Open+Sans' rel=’stylesheet’ type=’text/css’>
<link href=’https://fonts.googleapis.com/css?family=Lato:100italic' rel=’stylesheet’ type=’text/css’>
<link rel=”stylesheet” href=”dist/styles/main.css”>
<script type=”text/javascript” src=”dist/Repos.js”></script>
</head>
<body>
<script type=”text/javascript”>
var app = Elm.Repos.fullscreen();
</script>
</body>
</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.
However I do like reddit as a platform so feel free to shout here:
/r/elm/comments/4jhaxf/elm_017_successful_upgrade_of_real_world_app_and/