Elm is said to be easy to learn and use. Well, this is what we’re going to see by implementing String Calculator Kata with Test Driven Development. Rules are described here: http://osherove.com/tdd-kata-1/. For this exercise, we’ll only focus on the language syntax and patterns.
Getting the environment up and running
I’ve followed the official guide and chose the npm package to get Elm on my machine.
npm install -g elm will give you access to the following command line tools (of course, Node and npm are required to use this method):
elm-repl: play with Elm expression
elm-reactor: get a project going quickly
elm-make: compile Elm code directly
elm-package: download packages
It’s no more complicated than that. Only
elm-package will be used in the context of this kata exercise.
Testing Elm code
As we’ll explore Elm with TDD, we need to find how to quickly run unit tests to get immediate feedback. Bonus point if they can automatically re-execute themselves as we write and update code (watch mode).
After a quick search, elm-test appears to be a solid candidate. It provides an API (
fuzz) and the necessary tooling to run tests locally in a terminal. Install it with the following command:
npm install -g elm-test.
Init the project
Initiating a project is really easy. All we need to do is create an empty directory,
cd into it and run the following command:
elm-test init. Here’s what we will get:
elm-package.json describes the project’s informations and lists the required dependencies. These are downloaded in the
Example.elm shows how to create a test suite. First test is not implemented and if we try to run the suite with
elm-test command, terminal output message will be quite informative.
Implementing String Calculator Kata
A code kata is an exercise in programming which helps a programmer hone their skills through practice and repetition. The term was probably first coined by Dave Thomas, co-author of the book The Pragmatic Programmer, in a bow to the Japanese concept of kata in the martial arts. — Wikipedia
In Test Driven Development, we move forward with baby steps. First step is to write a failing test. Then, make this test pass with a small amount of production code and then refactor. Finally, repeat the loop until all requirements are met. Shall we get started?
First thought: syntax is weird
In the String Calculator Kata, first step is to create a simple
add method that takes 0, 1, or 2 numbers (as a string) and returns their sum. For example,
add("") should return
Let’s write the first failing test:
elm-test —-watchis the way to run this test suite with watch mode. It means every time we’ll write or update code, a tests run will automatically be triggered. Pretty neat!
First things first. Ok,
import statements are quite familiar, but, what is this lack of parentheses and curly brackets? It appears Elm uses whitespace and indentations instead of those (although parentheses are necessary in specific cases).
test is a function that takes a first argument as a string and a second argument as a function that evaluates a single
test is a function that returns a function. Indeed, functions are curried by default with Elm).
Next, what the heck is this:
<|\() ->? Well,
<| is used to reduce parentheses usage. For example,
leftAligned (monospace (fromString "code")) can also be written that way:
leftAligned <| monospace <| fromString "code". Isn’t it better (I guess…)?
\() -> is used to implement anonymous function. The empty parentheses are what’s called a unit type. A type that can only ever have a single value (which is
\_ -> can also be used where
_ is a placeholder for any value.
Now, let’s write the code to make this test pass:
Done. Baby step! Let’s go a bit further, shall we? No need to refactor yet, here’s a second failing test where we will try to get the sum of 2 numbers:
Using List and Result
I didn’t get the solution for this test right away. Checking if the string has a
, and then split is quite obvious but I had to understand how
List works and how to convert
Int with Elm.
Here, we use the
map method to convert our
String number to an
String has a built-in function (
toInt) to do this task, but the thing is that it returns an object of type
Result is an object that’s either
Ok (when task succeeded) or
Err (when task failed). Then, all we need is to return the actual value when
Ok or a default value (
Err. Exactly what
Result.withDefault function does.
Finally, we use the
List.sum method to get the sum of our
Int numbers. Notice the use of
|> to reduce parentheses usages. The output of the left part is the input of the right part. I personally find it very convenient and logical after a bit of practice.
What we can do here to refactor is create a function
splitWithSeparator whose responsibility is to check if a string has a given separator and then split this string into a list.
In the case where the string of numbers doesn’t contain any separator (it means there’s only one number), we just append the number to a list, so that in both cases,
splitWithSeparator will return a
List (in Elm, a function has to return the same type in every branch of code).
Handling new lines between numbers
Next step of the kata is to allow
add method to handle new line character
\n between numbers, so that for example,
add("1\n2,3") will return
Let’s add another new failing test for this case:
My first attempt to make this test pass was to split the string with
\n as separator and then split again each chunk of string with
, as separator, which gives us a list of lists of strings. That’s not very elegant (actually not at all) but it works, thanks to
List.concatMap that maps its given function onto a list and flatten the resulting lists.
Regex to the rescue
Although the solution with
concatMap works, we can do better. How about using a simple regex to split our string of numbers wherever there’s either
, as separator? Let’s refactor the
Regex.split splits a string into a list using a given
Regex.regex separator. It also needs to know how many matches we want to make. Here we need to find all the characters that match the following pattern:
\\n|,. So, we use the
Regex.HowMany data structure value:
Regex.All. Our method is greatly simplified, isn’t it?
Handling custom separator
I think this step is the hardest but hang in there! So now we need to handle custom separator in our
add method. A custom separator should be specified at the beginning of the string of numbers, the following way:
"//[separator]\n[numbers]". For example:
add("//;\n1;2;3") should return
6. Of course, this should be optional and all previous scenarios should still be supported. Let’s repeat the loop and add the failing test:
Here, what comes to mind first is to check if the string contains a custom separator, extract it if present and then repeat the split and convert steps. How does that sound? Ok, get ready, we’re about to get our hands dirty!
What do we have here. 3 new functions:
removeSeparatorPattern. Also, there’s this
let ... in thing we’ll explain a bit further.
startsWithSeparator is quite easy, we’ll use
Regex again but this time with the
extractSeparator is a bit touchy. Notice the new regex we’re using:
^//(.+)\\n (see above). Parentheses are here to capture any character(s) that will be between
\n. This is our custom separator.
Regex.AtMost 1 means we’re looking for 1 match only.
Regex.find returns a list of
Match objects. Each
Match contains a
submatches field, which is a list of subpattern(s), the pattern(s) surrounded by parentheses. It can be empty as all regex don’t have subpattern. By using
List.concapMap, the lists of submatches are flatten into an unique list. Then, we can extract our custom separator string with
List.head, which gives us the first item (in our case, there will always be one item).
unwrap function is used to extract the current value of a
Maybe wrapper. Here, it’s kind of unsafe because if there’s no value (
Nothing), the program will crash. However, in our case, it should never happen. We need to apply this util function twice because our custom separator is actually wrapped twice in a
Maybe object. Indeed,
submatches is a list of strings enclosed in
Maybe objects, and
List.head returns the head of the list as a
Maybe object as well.
Let’s take a step back to
let can be used to declare variables. Well, it’s the same with Elm except that the declared variables have to be used within a
in block. It might seem a bit weird at first but it’s a good way to enclose variables, as they’re not available outside their block.
removeSeparatorPattern will return a new string, without the custom separator at the beginning. It’s quite straight forward:
Now (if you’re still here…), all of our tests should pass but the
add method got a little bit complicated. What we could do is create a function to extract the list of string numbers (with or without custom separator) and let the
add method only calculate the sum of these numbers.
As you may notice, we remove the second argument of
startsWithSeparator function. It’s actually not necessary as we’re always looking for the same
extractSeparator is renamed with
extractCustomSeparator for more clarity, and its second argument removed as well (we use the hard coded variable
customSeparator within the function). Also, we add a
splitWithString function as we don’t need regex to split a string of numbers with a given string separator.
As a result, here’s the simplified
Negatives are not allowed
This is the last rule for this kata. Calling
add with a negative number should throw the following exception : “Negatives are not allowed” , followed with the negative number (a list if there are multiple negatives). Actually, there are 4 more steps we could add, but that’s plenty enough for this article…
Before writing the corresponding failing test, let’s look at how to handle errors / exceptions with Elm. By searching through the official documentation, we can find an interesting object:
Okmeaning the computation succeeded, or it is an
Errmeaning that there was some failure.
By running this unit test, that obviously fails, we get the following error message:
I like how informative it is. Indeed, the
add method currently returns an
Int object whereas it should be a
Result (wrapping a string when
Err and an
First, we need to refactor all of our previous tests so that the assertions look like this:
Expect.equal (StringCalc.add "1,2") (Result.Ok 3). Now, all tests should fail, but that’s ok. All we need to do to make them pass is change the return type and value of the
All right, we’re back on track! Only the last one with the negative number should now fail.
Let’s write a function to check if there’s a negative number in the list. It’s quite simple thanks to
List API (one line’s enough):
Now, let’s update the
add method with this new piece of code:
As you may have noticed,
containsNegative expects a list of
Int numbers. Thus, we update
extractNumbers to return a list of
Int (instead of
String) numbers .
Test should be green! Ok, it will obviously fail if the negative number is different than -1 but, remember, baby steps!
Refactoring again² (lightly)
extractNumberList we duplicated some code:
|> List.map toInt. Indeed, it is necessary in both branches (in case numbers list start with custom separator or not). One simple way to remove this code smell is to extract it into a function that we can use within the
Handling negatives list
Here’s the last failing test we’ll add for this kata:
2 steps are required to make it pass: extract the negative numbers and display them in the error message. Let’s repeat the loop and implement these functions:
add method should look like this:
Finally, all our tests should pass. Isn’t it satisfying?! 😎
All the code is available on my ⭐️ Github ⭐️. I tried to separate each step with a different commit, which should give you a good overview of progress.
I think TDD (if you’re experienced enough with it) is a good way to learn a new language. Although, it might be a good idea to start with a code kata you’re familiar with. I didn’t know String Calculator and I struggled quite a bit at first, having to understand Elm stuffs and figuring out how to fulfil the kata requirements at the same time.
Please, feel free to add feedback, I’m sure there are better ways (more Elm-ish) to handle this coding exercise. I’d love to hear more about it from Elm developers.