Introduction to Elm programming language for React developers
Taking a short break from the Let's code a Virtual DOM! series, I decided to write an article on the technology that has inspired me to create my own virtual DOM in the first place.
That technology is a programming language called Elm. It can be used to build highly reactive front-end applications.
What makes it different from Javascript or Typescript?
Why should you care?
It's a purely functional statically typed programming language - a dialect of haskell.
If at this point, you are a bit sceptical about it - that's fine. Functional languages tend to be complex and hard to learn. But Elm is different.
Its simplicity makes it easy to learn - you are not expected to know maths or anything from category theory upfront. Of course, there will be some new things to learn (and probably unlearn too).
It uses a virtual DOM and redux-like state management to build reactive UIs (Redux was even inspired by Elm). So if you are a React developer you'll feel right at home!
Elm is simple and logical. Its compiler will guide you along the way - almost like a pair-programming buddy. All that makes it a perfect language to learn functional programming.
Why else should you bother to give Elm a try?
No Runtime Exceptions
How many times have you encountered the following errors in your Javascript applications:
TypeError: undefined is not a function
, TypeError: Cannot read property 'value' of null
?
Elm is advertising that there are "No Runtime Exceptions" in applications built with it. It's because the compiler will catch all those errors in the build time. It can guarantee that if the code compiles, it will not crash.
Excellent compiler
In Elm, the compiler is your best friend. On the top of the "no runtime exceptions" promise, it's known of well thought and helpful error messages.
It will guide you by hand, and will even offer some suggestions as to how to fix the error!
Enforced app structure
In React there are a million ways to do the same thing. Let's think about state management. Redux, Flux, MobX, Recoil and other libraries are used to do the same thing in a slightly different manner. If you happen to change a project, and it uses the library you don't know it means that you need to waste your time learning the new technology again.
In Elm, all applications look the same. All follow the same pattern. If you know how to work with one Elm app, you will quickly be able to switch between different applications without having to worry about re-learning everything from scratch.
Great performance
Elm is fast. As we've mentioned before, it uses a custom implementation of a virtual DOM, that has been designed for speed and simplicity. Elm compiles to Javascript but produces small assets, which can boost the performance of your app.
Show me the code!
If all of that convinces you to give Elm a try, it's high time to look at some code.
The easiest way to start working with Elm is to use an Ellie editor.
Feel free to open this link in a new tab and follow along.
Imports
Imports, like in many languages are placed on the top of the file. In Elm, each file is a module and needs to have a unique name. It's set in the first line of the file, along with a list of exposed (or exported in JS term) functions.
module Main exposing (main)
Type declarations
In Elm we are able to create custom types like this:
type alias Model =
{ count : Int }
This is an equivalent of Typescript's:
type Model = { count: number }
Note the alias
keyword. It's very important here, as Elm also allows us to create custom types that will be treated as values. The syntax for them can be seen below:
type Msg
= Increment
| Decrement
Declaring functions
In Elm, everything is a function.
Let's take for example initialModel
:
initialModel =
{ count = 0 }
Even though it looks like a variable assignment, in reality, it's a function. In Typescript it would look like this:
const initialModel = () => ({ count: 0 })
If the function takes any arguments, they're placed before the =
sign.
update msg model =
-- ...
| --
is a comment in elm
Type annotations
In the application generated by Ellie, over each function you can find a Type annotation for that function. They're optional but highly encouraged.
update : Msg -> Model -> Model
update msg model =
-- ...
It can be a little confusing at first, but Msg -> Model -> Model
means that the first argument is of type Msg
, the second is of type Model
and the function returns a Model
type.
It's like that because of a partial application, a pattern found in many functional languages.
The application architecture
A basic Elm application consists of three functions: view
, init
and update
.
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
init
is the initial state of the model of our application.view
is the function that renders UI onto the screenupdate
is a function fired every time a Msg (message or action in Redux) is sent. It would be a reducer in React world.
The view
function
The view
function is a core of the app in Elm. It takes a Model
as its argument and returns an Html Msg
type.
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+1" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
]
What would this code look like in React?
const view = (model: Model) => (
<div>
<button onClick={() => dispatch('Increment')}>
+1
</button>
<div>
{String(model.count)}
</div>
<button onClick={() => dispatch('Decrement')}>
-1
</button>
</div>
)
The result of this code is a simple counter.
Now, what happens when someone clicks the button?
Dispatching events
Let's focus on the part when an event is dispatched. It's happening in the onClick
attribute of a button.
button [ onClick Increment ] [ text "+1" ]
The onClick Increment
part is dispatching the Increment
action on a button click.
Actions are called Messages in Elm. Two messages - Increment and Decrement - were defined in the code above:
type Msg
= Increment
| Decrement
Those messages are sent to the update
function automatically every time the onClick
handler is fired. No need to manually dispatch them.
The update
function
The state is managed in the update
function. Every time a new message is sent, the update function updates the state.
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
The update function takes two arguments, the first is a message sent, and the second is a previous model.
How could it be written in typescript?
const update = (msg: Msg, model: Model): Model => {
switch (msg) {
case 'Increment':
return { ...model, count: model.count + 1 }
case 'Decrement':
return { ...model, count: model.count - 1 }
default:
return model
}
}
It behaves exactly like a reducer function known from Redux or the useReducer
hook.
What's nice, Elm will make sure that all possible outcomes are handled properly:
Let's add a new Message Reset
:
type Msg
= Increment
| Decrement
| Reset
Now if we run the code we'll get the following compiler error:
The compiler is telling us what we have missed!
Conclusion
I hope that this short introduction got you interested in learning Elm. It's a fantastic language that makes development much more interesting. Of course, it will not replace React or Javascript. But it can help you to develop good practices and learn new approaches to software development.
If you would like to play around with the Elm code, I encourage you to try the following task:
| Add a reset button to the page
This will involve editing the view
function, updating the Msg
type and adjusting the update
function to reset the counter.
If you are interested in learning more about Elm, let me know. I will be happy to continue this topic on this blog.
Thank you for your time, and see you in the next post 😊
Subscribe to my newsletter
Read articles from Krzysztof Kałamarski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Krzysztof Kałamarski
Krzysztof Kałamarski
I'm a fullstack javascript developer from Wrocław, Poland, with over 10 years of experience in building high-quality IT solutions. Although my expertise is in the JS ecosystem, I'm always curious about other technologies. In my spare time, I take care of my allotment garden. I'm a huge coffee nerd. MBTI: INTP-A I speak five languages: Polish, English, Spanish, Portuguese and Bulgarian. If you happen to speak one of these, please feel free to reach out to me 👋