Elm: First Taste of Syntax

Get a first taste of Elm's syntax in this kata to implement your own map function. We'll start with a failing test suite, work through first errors, make the tests pass, explain basic tidbits of syntax along the way.
This exercise was adapted from the "Accumulate" exercism. If you'd like to follow along, you can clone this elm-kata-map repo.
The Test Suite
The test cases in Test.elm that we'll start with are these:
import Map exposing (map)
-- ...
square x =
x * x
describe "Map"
[ test "[] Map" <|
\() -> Expect.equal [] (map square [])
, test "square Map" <|
\() -> Expect.equal [ 1, 4, 9 ] (map square [ 1, 2, 3 ])
, test "toUpper Map" <|
\() ->
Expect.equal [ "HELLO", "WORLD" ]
(map String.toUpper [ "hello", "world" ])
, test "reverse Map" <|
\() ->
Expect.equal [ "olleh", "dlrow" ]
(map String.reverse [ "hello", "world" ])
]Expect.equalis our assertion. Its first arg is the expected output. The second arg is the exercise of our subject under test with specific input.- It looks like the tests are telling us we want to create a
mapfunction that takes a transform function and aList. Callingmapwith these inputs will return a new, transformedList.
Error #1 - File
To run the tests, we're using elm-test. To install the test runner, npm install -g elm-tests. To run the tests, run elm-test in the project directory. The first test run will yield this error:
I cannot find module 'Map'.
Module 'Tests' is trying to import it.We have not created our source file yet for Map.elm. Once we create it in src/Map.elm and run the tests again, the error changes.
Error #2 - Module
Now the error is:
The module name is messed up for ./../src/Map.elm
According to the file's name it should be Map
According to the source code it should be Main
Which is it?In Elm, file names are significant and tied to the module name. Since the file is named Map.elm, we'll call our module that as well, making the first line of src/Map.elm read:
module Map exposing (..)Mapis the name of the module- The
exposingclause allows you to export a public interface from the file (..)indicates you want all declarations exported
Error #3 - Declaration
Let's comment out all but the first test. That seems like a smart thing to do, right? :) -- is a line comment in Elm. Leaving just one test running will help us focus on implementing only what's needed for it to pass. Re-run the tests. The error is now different.
I ran into something unexpected when parsing your code!
I am looking for one of the following things:
"{-|"
a definition or type annotation
...This means that we have yet to make our module do anything. There are no declarations in the file. Let's make our first. Look at the first test case in Test.elm to decide the API that the test requires:
(map square [])mapis the name of a function- Functions are invoked by following the function name with space-separated arguments.
- The first arg is a function that takes input and maps to another output.
- The second arg is a
List. This is a core type -- ordered, homogenous, but not indexable. We have to deal in sublists withList. We'll deal with this later.
Declare our Function
So we declare the map function in our src/Map.elm file to match this expected use:
map transform list =
[]- The name of function is first.
- Space-separated parameters come before the equal sign.
- The equals sign will bind the following expression to the identifier.
- This function is hardcoded to always return an empty
List([]).
Now it should compile and pass the first test.
Type Annotation
The compiler can infer types. We can also make them explicit. Sometimes this is to help the compiler. It is always to help the reader. Add this to the line directly before your function declaration:
map : (a -> b) -> List a -> List b- First comes the name of the function.
- Separated by
:colon - Then comes the list of parameters, separated by
->arrows. - The final "parameter" is actually the return type; ie
List b - Parameters are listed as types only, no variable names.
aandbare generic types. By convention, generics start withaand go in alpha order.- The type
(a -> b)is a function that takes one type (a) and returns another (b). List ais aListof generic typea
Pattern Matching
Now uncomment the next test and re-run the tests. It should fail. Now we need to deal with the test case of an non-empty list:
Expect.equal [ 1, 4, 9 ] (map square [ 1, 2, 3 ])We're going to setup the structure for solving this problem with pattern matching. Elm has great pattern matching capabilities. Think of it as conditions, as in if-else expressions, but with more powerful matching. A case-of expression is used to invoke pattern matching. Add this to the body of your map function:
case list of
[] ->
[]
_ ->
[ 1, 4, 9 ]- Patterns are registered as
{expression} -> {expression}after thecase. - The pattern can be a literal, eg
[]. _is the default/catch-all case.
To get this to compile and pass the 2nd test, we'll need to temporarily adjust the type annotation to allow returning a List number. This step was just to push the TDD a little further, so this feels a bit artificial. Never fear, we're about to finish up with the meat of the function. Now uncomment the other two tests, and let's finish the real pattern mathing. Now we have to handle arbitrary lists and transforms:
Expect.equal [ "HELLO", "WORLD" ] (map String.toUpper [ "hello", "world" ])
Expect.equal [ "olleh", "dlrow" ] (map String.reverse [ "hello", "world" ])Finish the Solution
Replacing the 2nd pattern in the case-of expression, we can handle all inputs. The final state of src/Map.elm is this:
module Map exposing (..)
map : (a -> b) -> List a -> List b
map transform list =
case list of
[] ->
[]
head :: tail ->
transform head :: map transform tailhead :: tailmatches a non-emptylistvariable.::is an operator for adding an element to the front of a list. So this pattern can be thought of as "Is there an element at the front of the list?"- The pattern binds the
headandtailvariables to the actual head (first, single element) and the tail (Listof remaining elements). Those variables are available to use in the expression after the->. transform headchanges just the first element of the originallist.- The call to
mapis recursive. We send thetransformfunction straight through.tailbecomes a new list (because the first element has already been transformed). - As the stack returns, the new list will concat using the
::operator.
We did it! Re-running all the tests, they pass. We had a good first taste of Elm syntax along the way, exercised in a short test-driven exercise. How was the taste to you? Did you like it?