Spotted Arrow

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.

Elm It Up 🚀

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! 👻

Summary 🏁

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:

In Gist form: