2017-09-07
Let's Write Some Elm
If you’re a JavaScript developer you probably have been hearing about a language called Elm. You might have heard it’s some kind of typed functional language that compiles down to JavaScript and runs in the browser. If that is what you heard, then you would be right 😉.
Elm is a great language to write but what makes it nice to use is not necessarily the syntax, although it’s great, or its ecosystem, which is awesome, or its built in architecture (TEA), which is pretty cool. What is nice about Elm is the developer experience. And a critical part of the developer experience is debugging; and Elm, in all its glory, has excellent error and warning messages. These are not your run of the mill errors like x is not a function
, x is undefined
, or x.map is not a function
. Instead you get nice errors or warnings that even provide helpful recommendations.
Let’s learn this first hand by heading on over to the friendly Elm editor Ellie:
When you click on the link above you will be taken to an editor with a fully functional Elm environment. At this point your code looks something like this.
module Main exposing (..)
import Html exposing (Html, text)
main : Html a
main =
text "Hello, World!"
Let’s create a function that capitalizes a sentence while also lower casing everything else. I think this type of function will give you a good idea about Elm and how it works and afterwards I’ll create some intentional errors. Call this function capitalize
and pass it one argument which is a String
.
capitalize string = string
This simple function doesn’t do what you want it to…yet. Instead, you are passing a string and returning it. But did you notice capitalize
is underlined. Hover over capitalize
and see what it’s barking about.
Top-level value capitalize does not have a type annotation.
I inferred the type annotation so you can copy it into your code:
capitalize: a -> a
That is the compiler, your friendly neighbor, telling you that you are running around without a type annotation for your function. How nice of it 🤗! What the compiler provides is a type of a
to fill in this mystery. By convention, a
is intended to denote a type of any
, which could be a String
, List
, Array
, etc. In this case though, what do you think is the type you are actually handling? A string! So instead of a general type of a
, let’s annotate the function with type String
.
capitalize : String -> String
capitalize string = string
Typically when I create a capitalize function I take the first letter in the string, capitalize it, and combine it with the rest of the string uncapitalized. In JavaScript I might do something like this:
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1).toLowerCase();
}
Pretty simple, huh? Do you see any problems with this type of function? What if you have an empty string like in the repl.it example below? Click run
to see the output.
You get a big error! In the console you should see something like this:
TypeError: Cannot read property 'toUpperCase' of undefined
I didn’t handle cases for when the string is empty at index 1. If this was a real life situation the browser would be broken and the user pissed! How might this type of situation be handled in Elm. Let’s write a similar function the Elm way.
First let’s import a couple functions from Elm’s String
library, which is part of core. You don’t have to install anything but you do need to import it explicitly at the top of your module.
import String exposing (uncons, fromChar, toUpper, toLower)
When you click compile you should see another warning.
Module String is unused.
Best to remove it. Don’t save code quality for later!
How sweet 😘! The compiler cares about my code quality as much as I do. Well, in this case, I know I am going to use these functions at some point. But good looking out!
If you read the documentation for the String
library, specifically the uncons
function, you will notice something a bit different.
When you see the type annotation for uncons
you see this:
uncons : String -> Maybe (Char, String)
What is a Maybe
? I am sorry, but no burrito 🌯 monad story for you! Let’s just say that when you try and split a string into a head
, the first letter, and a tail
, the rest of the string, there is a chance that you won’t get what you are expecting. As in the case with the JavaScript implementation, there was not a letter at index 1, so it failed with a run-time TypeError. Elm is being a nice helper here and providing you a Maybe
and putting the onus on you as a developer to handle such cases for when a string is empty. So how do we handle such cases? Let’s figure it out!
When you reread the documentation you will see that you handle two cases: Just
and Nothing
.
uncons "abc" == Just ('a',"bc")
uncons "" == Nothing
Let’s now wrap this neatly into the function you have currently. In this situation you can use a case statement to pattern match on the return type of uncons
. Let me show you 👇
capitalize : String -> String
capitalize string =
case uncons string of
Just tuple ->
""
Nothing ->
""
I know that uncons
will give me back a Maybe
of either Just
with a tuple of a Char
and a String
(i.e. Just ('a', "bc")
, or a Nothing
. The ''
denotes a Char
and ""
a String
. The (someValue, someOtherValue)
denotes a tuple of two values. This is still not doing anything but you are handling both cases at least. When you click compile it should run as expected.
But this is not good enough. You can do better. The tuple value provides everything you need to capitalize the first letter and append the tail to it. Let’s first get the elements from the tuple. You will need the Tuple
library for this.
import Tuple exposing (first, second)
Then you can update the function using a let
expression assigning the first and second value in the tuple. This is how I did it:
capitalize : String -> String
capitalize string =
case uncons string of
Just tuple ->
let
head = first tuple
tail = second tuple
in
""
Nothing ->
""
Easy peezy, right? You are taking the tuple and splitting it. But don’t forget your types! At this point head
is a Char
and tail
is a String
. You can’t automagically append these items together. Let’s use a helper function from the String
library to convert the Char
into a string.
head = first tuple |> fromChar
That rigamarole, |>, is called a pipe operator. It let’s you take a calculation and pipe into another function. COOooOOooOOl! But you need to do one more thing: uppercase the letter. Use the pipe operator!
head = first tuple |> fromChar |> toUpper
Do the same for the tail, but in this case lower ‘em all.
tail = second tuple |> toLower
Now that you have two strings, let’s try and append them.
Just tuple ->
let
head = first tuple |> fromChar |> toUpper
tail = second tuple |> toLower
in
head ++ tail
Okay, almost at the finish line. Let’s take the text you were presented with at the start and capitalize it.
main : Html a
main =
text <| capitalize "Hello, World!"
I didn’t tell you that you could reverse pipe, but you can reverse pipe 😎. If you compile this code you should see Hello, world!
in the browser.
I walked you through writing this code but let’s just say I wasn’t here holding and caressing your hand 🤝. Instead imagine you were getting a String
for the first value in that tuple. Take out fromChar
, compile, and let’s see what happens.
head = first tuple |> toUpper
Uh-oh! You have a type mismatch.
The right side of (|>) is causing a type mismatch. (|>) is expecting the right side to be a:
Char -> a
But the right side is:
String -> String
Hint: With operators like (|>) I always check the left side first. If it seems fine, I assume it is correct and check the right side. So the problem may be in how the left and right arguments interact.
Hmmm, interesting 🤔. That is pretty good feedback. If you were to reread the docs you would see that the error is correct. At some point you would realize that you have to transform your type to correctly match, and I think Elm would gradually steer you in that direction.
What if though, you were working in a large codebase and trying to refactor some old Elm code. If there is such a thing 😁. And instead the type definition for your function was:
capitalize : String -> Int
You are refactoring away and when you finish you see a big compiler error in your terminal. Uh-oh, another type mismatch:
The definition of capitalize does not match its type annotation. The type annotation for capitalize says it always returns:
Int
But the returned value (shown above) is a:
String
You think, oh that’s not right, it is a String
. Again, Elm has rescued you from the sorrow of run-time errors! Type annotations are optional because Elm can infer the types for you, but it is best practice to declare your types so the values you create are what you expect.
For funssies pass in an empty string into capitalize
and see what happens. The browser is alive, still! 👻
In this lesson you learned about Elm, the typed functional language that compiles down to JavaScript. You learned about the boundaries it imposes on you as a developer, which from my perspective, leads to cleaner, more maintainable code. You also learned about Elm’s wonderful error messages that are actually helpful. And overall, I hoped you learned how Elm strives to provide an excellent developer experience, for the noobs and experts alike. Until next time, keep hacking!
Check out the complete code snippet below:
{
"version": "1.0.0",
"summary": "Tell the world about your project!",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": ["."],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.1.1 <= v < 5.1.1",
"elm-lang/html": "2.0.0 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}
<html>
<head>
<style>
html {
background: #f7f7f7;
color: red;
}
</style>
</head>
<body>
<script>
var app = Elm.Main.fullscreen();
</script>
</body>
</html>
module Main exposing (..)
import Html exposing (Html, text)
import String exposing (uncons, fromChar, toUpper, toLower)
import Tuple exposing (first, second)
capitalize : String -> String
capitalize string =
case uncons string of
Just tuple ->
let
head = first tuple |> fromChar |> toUpper
tail = second tuple |> toLower
in
head ++ tail
Nothing ->
""
main : Html a
main =
text <| capitalize "Hello, World!"
This article has Webmentions