Good friend of mine said I should learn Elm.
— “You’ll get much from it doesn’t matter if you’ll be using it or not.”
And that was exactly that kind of friend who no matter what he says always sounds convincing. Probably because of his white beard and blue eyes. Or may be because he is actually almost always right.
Elm is 4 year-old grown-up which transpiles into javascript and doesn’t have js fatigue of choosing the ‘right’ framework because actually it is a framework. It is completely functional and is a father of redux:
So I sacrificed several promising-to-be-good weekends to get a portion of insights and now happy to share them with you:
I will start straight with the hardcore concepts.
You can forget about `NaN`, ‘Uncaught TypeError’ or ‘foo is undefined’ annoying error messages in Elm, because it supports static type checking which means that at compilation time it checks all of your code to make sure that all of the variables and inputs and outputs from functions match up. This means that if your function accepts only String there is no way you can pass anything else.
And although it is recommended to declare the types like:
reps : String -> Int
(reps is a function name, String is input type and Int is output type), it is not required, because Elm can automagically figure out all the types itself(which is called type inference) and show you appropriate error or success messages after compilation is finished.
One good trick commonly used in Elm — is Union type which allows you to define possible values for the weird or complex type. Say we have an Exercise type which can be one of the following strings: Pull-up
or Push-up
type Exercise = Pull-up | Push-up
and want to make a function reps
which returns number of reps based on the exercise:
reps : Exercise -> Intreps exercise = case exercise of Pull-up -> 10
Push-up -> 15
What’s important here is that whenever we pass a Union variable as a parameter(which is exercise
in this example) we are forced to use case
expression. And what’s more important is that all the branches of that case
expression are checked by compiler. Which means that:
Muscle-up
with 8 reps), you get an error at compile time.Psuh-up
) by accident, you get an error at compile time. Nice, huh?And we are not finished with types yet.
Even for me who came to developer world after years of writing requirements specifications and who understand the importance of edge cases, sometimes difficult not to forget them in code. What if value is undefined
or json format is incorrect or http request is timed out?
Elm takes care of this by introducing Maybe, Result and Task types which are special cases of already familiar Union types.
If you have optional field you’ll probably use Maybe:
type alias Sportsman = { name : String , age : Maybe Int }
which means here that age
field can present one of 2 values: Nothing
or Just Int
—first is empty and second is non-empty value. And because it is Union type, any time you pass this var as a parameter you’re forced to use case
expression and handle all the variations so you cannot avoid handling null/undefined case:
getReps : Sportsman -> Maybe IntgetReps sportsman = case sportsman.age of Nothing -> Nothing Just age -> Just 12
Same approach is used for Result
and Task
types but instead of optional fields Task
is used for sync operations like parsing json, and Result
is used for async operations like html requests. Both of them can end up in one of the following values: Success
for successful cases and Failure
for error cases and same as with Maybe
we’re always forced to handle both cases.
Another consequence of static typing is automatically enforced versioning.
This means that if we create a library and want to share it with others we don’t have to think about its version numbers every time we update our package. Elm will do it for us.
Starting from initial version 1.0.0 it will compare code changes for each of the consequent releases of our package and create new version number which does most accurately reflect the changes we made. 🌟
Redux is a well known pattern in Javascript nowadays originated in Elm. You can find it in React and Angular and I even recently used it in a jQuery project without any modern MV* frameworks.
The idea is you have one single point of trust which is called Model
(Elm) or State(React) or Store(Angular) and you have an interface to update it. In Elm we call this interfaceUpdate
(which is Reducer in Javascript world).
So we call Update
function each time we want to change the Model
.
Update function gets current Model
and Msg
(Action) as parameters and returns the new Model
.
Lately we use Model
to update our view which is simply Html
if we are talking about website, and we do it by calling the self-titled function — View
.
That’s how it looks:
Elm architecture
And why is it cool?
First of all QA becomes much-much easier.
We already know Elm is a functional language which means it provides us 2 functional guarantees:
And basically this means that having a list of Update
function calls we always can rerun them and will always get the same Model
as a result of rerun. Why? Because no one can change our Model
from outside(remember? no side effects) and because our Update
function always returns the same result if called with the same parameters.
And this means that any time your QA engineer finds a bug she sends you the log of Update
function calls(which is silently recorded) without manually documenting all the steps. And you instead of manually reproducing all the steps just import it in your app and immediately see the bug on the screen with possibility to move back and forth in the history of actions to see which chain of user actions led to the error. 🌟🌟
Second fruit of using redux is lazy loading.
What I like about modern javascript frameworks comparing to jQuery is they are declarative. You don’t say in your program how to manipulate the DOM — append, remove, hide, etc. You just describe initial and final states of html — how it should look in different situations, and framework takes care(normally with Virtual DOM) of everything required to update the page so it looks like you want it to.
Same for Elm. You simply define the View
function describing how html shall be rendered based on the current Model
. And then when Model
is changed your View
function updates the DOM accordingly comparing current and previous states and making corresponding DOM manipulations.
Now interesting thing: what if only small part of the Model
was updated and rest of it stayed unchanged? Does it make sense to recalculate and compare the whole DOM? Surely not.
If we break our view function into pieces so that one function renders header, another renders footer and etc then because of the first function guarantee(always return the same result given the same argument values), we can skip the functions whose input wasn’t changed.
We just put lazy
keyword in front our function:
viewHeader: String -> Html Msg viewHeader name = ...some code
view : Model -> Html Msgview model = lazy viewHeader model.loggedUser.name ...other code
Here we have a header which shows logged in user name. Once something is changed in the model(user scrolls down and new content is loaded) we don’t want to recalculate header part of the DOM because it is basically same as before. But we want to update content part, because user scrolled it. That’s what lazy
do for us — Elm will check that viewHeader input wasn’t changed and skip this part completely but will render the rest. This saves some CPU resources for us and makes rendering faster.
And this leads to another important concept of Elm which is immutability. In previous example we had String
as an input for our Header function but what if we had something more complex — Object
?
What if we want to compare 2 input objects(new and old)? Then we have to go deep into their fields hierarchy and compare fields one by one. Which is time and resource consuming.
On the other side with immutability which requires to create new object each time we change something in it, comparison is as simple as comparing two references which is blazingly fast. Because if references point to the same object then it is the same object and if not then these are two different objects. Cool. Now lazy loading makes even more sense.
But we can do even faster.
We all know that typical browser performs repaint 60 times per second as this is how our eyes perceive visual information. There is no need to do repaint more often, because we won’t improve any website look and feel but will be wasting device resources.
At the same time we want other operations apart from repaint to work in normal mode either it is an http request or some complex calculations in the background.
So how we can achieve this in Elm?
Well, we actually don’t have to do anything. It is built-in. Elm uses requestAnimationFrame
under the hood to sync view calculations with browser repaints. This means that view function which renders html will not be called more often than 60 times per second and it saves time, CPU resources and mobile device battery.
The main reason we can do this in Elm is second function guarantee applied to the View
function — it doesn’t have any side effects so the only thing it alters — is html. Which means it cannot update the Model
. And while we make View
function calls to happen only 60 times per second, our Model
still gets updated in real-time without any delays. That’s the purity of pure functions.
As we already can see Elm spends a lot of efforts to prevent errors in our applications so the testing framework used in it is quite elaborate too.
One feature I’m excited about and which is coming soon in Mocha is fuzz testing.
Idea behind it is running the test case many or several times with randomly generated input. Cool part is all edge cases like zero, empty or negative values are guaranteed to be checked at least once and you can also set some ranges along with their probabilities to be sure that the most important cases are tested more often than others.
Fuzz.frequency[ ( 1, Fuzz.intRange -100 -1 ), ( 3, Fuzz.intRange 1 100 )]
This can save a lot of manual test writing time but has it’s caveat that tests execution time can be noticeably increased.
So once we know all these cool things, how can we use them in our normal Javascript life?
First, use static typing— Typescript or Flow could be great solutions for that. Typescript allows you to mix it with javascript in the same code base which means you can gradually move your project into static typed without breaking legacy code. It is also provides great IntelliSense support in combination with VSCode.
Second — use functional approach where appropriate(state management, rendering, etc). Redux library is a great example.
Third, use immutability with help of ready to use libraries like Mori or immutable.js to advance change detection and improve application performance.