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.equal
is 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
map
function that takes a transform function and aList
. Callingmap
with 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 (..)
Map
is the name of the module- The
exposing
clause 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 [])
map
is 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.
a
andb
are generic types. By convention, generics start witha
and go in alpha order.- The type
(a -> b)
is a function that takes one type (a
) and returns another (b
). List a
is aList
of 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 tail
head :: tail
matches a non-emptylist
variable.::
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
head
andtail
variables to the actual head (first, single element) and the tail (List
of remaining elements). Those variables are available to use in the expression after the->
. transform head
changes just the first element of the originallist
.- The call to
map
is recursive. We send thetransform
function straight through.tail
becomes 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?