In this article series, we tried to get into Elm as quickly as possible, by buiding three mini apps up to this point. In this article we’ll shift our focus on some more theoretical concepts, before we move onto the more advanced stuff.
Note: Examples in this article use Elm 0.19.
Working with List.map and List.filter
Elm works with immutable data structures. How do we then use the existing immutable data structure to find its members that satisfy only a certain condition or to produce different values based on existing values?
In other words, how do we filter a List? Or how do we even map over a List in Elm 0.19?
To achieve these two goals, we can use the map
and filter
functions. To keep things simple, we’ll look at the List.map
and List.filter
functions, although .map
and .filter
can also be used with some other data structures in Elm.
Let’s say our goal is to take a List
of numbers and find only those that are divisible by 3. To begin, let’s define a function that will take an Int
and return a Boolean
(either True
or False
), based on whether the number given to the function is divisible by 3. Navigate to http://elmrepl.cuberoot.in, and type the following function definition:
findThrees num = modBy 3 num == 0
The REPL will return:
<function> : Int -> Bool
Our findThrees
function takes an Int
and returns a Boolean
. Put differently, the expression modBy 3 num == 0
is evaluated first. Let’s say that num
is 3
, making the expression look like this: modBy 3 3 == 0
. This expression is true, and thus the expression evaluates to the value of True
, which is of type Boolean
. Next, this value is assigned to the findThrees
function.
In other words, if we call the findThrees
function and give it number 3
as its parameter, the findThrees
function will return the value of True
, which is of type Boolean
.
Next, let’s give our findThrees
function to our List.map
. The following code will not work. Try to guess why before reading the explanation:
List.map findThrees 3
The answer is: the number 3
is an Int
, not a List
.
This is what REPL tells us too:
> findThrees num = modBy 3 num == 0
<function> : Int -> Bool
> List.map findThrees 3
-- TYPE MISMATCH ----------------------------------------------------------- elm
The 2nd argument to `map` is not what I expect:
5| List.map findThrees 3
^
This argument is a number of type:
number
But `map` needs the 2nd argument to be:
List Int
Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!
Hint: Did you forget to add [] around it?
Obviously, we can’t give just a number
as the second argument of the List.map
function. Instead, to make this work, we need to give it a List
of numbers
. Like this:
List.map findThrees [1,2]
This time, success! REPL returns the following:
[False, False] : List Bool
Let’s try giving it a List of three numbers:
List.map findThrees [1,2,3]
This time, REPL returns:
[False,False,True] : List Bool
Next, let’s type a List of 10 numbers, and store it in a variable:
ourList = [1,2,3,4,5,6,7,8,9,10]
Running the preceding code in the REPL will return:
[1,2,3,4,5,6,7,8,9,10]
: List number
Looking at the preceding code, we can say that a List
of numbers
is stored in a variable we called ourList
. Now, let’s give the findThrees
function to the List.map
function, and pass the ourList
as the second argument:
List.map findThrees ourList
REPL returns a List
of Bool
values:
[False,False,True,False,False,True,False,False,True,False] : List Bool
Finally, let’s try to replace List.map
with List.filter
:
List.filter findThrees ourList
REPL returns a List
of Int
values:
[3,6,9] : List Int
Now that we have practiced using List.map
a little bit, let’s look at its anatomy. List.map
takes two arguments, the first one being a function, and the second one being the actual List
.
The function that is passed as the first argument to List.map
is used to convert the second argument (the List
) to a new List
, based on the logic in the function. List.map
does that by running the function we give it over each single member of the List
provided. This behavior of List.map
makes it a great candidate for improving our FizzBuzz app, which we built earlier in this article series.
For now, let’s run a List.map
in our Elm-REPL. In order to be able to run List.map
, we need to define a function it will use. So, let’s open Elm-REPL and define our custom fizzBuzzer
function:
fizzBuzzer number = \
if modBy 15 number == 0 then \
"fizzBuzz" \
else if modBy 5 number == 0 then \
"fizz" \
else if modBy 3 number == 0 then \
"buzz" \
else \
Debug.toString number
The backslash character - the \
- is used to break onto the next line in Elm REPL.
Let’s now examine an improved FizzBuzz app, implemented with the help of List.map
.
Here is the code:
module Main exposing (main)
import Browser
import Html exposing (text)
theList = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
fizzBuzz = "FizzBuzz"
fizz = "Fizz"
buzz = "Buzz"
fizzBuzzInput value =
if modBy 15 value == 0 then
fizzBuzz
else if modBy 5 value == 0 then
buzz
else if modBy 3 value == 0 then
fizz
else (String.fromInt value)
main =
text <|
Debug.toString <|
List.map fizzBuzzInput theList
Note the <|
operator. It simply means that we’ll start with List.map
, then do Debug.toString
, then do the text
function.
We’ll continue our examination of List.map
and List.filter
in Elm by building a few more mini apps.
List.map and List.filter are higher-order functions in Elm
List.map
and List.filter
are higher-order functions in Elm.
Higher-order functions are simply functions that operate on other functions.
Phrased differently, higher-order functions give superpowers.
To whom?
Of course, to regular functions! Which might just as easily be anonymous!
A JavaScript Example Using forEach
An example in JavaScript to kick things off:
var moguls = ['Jeff Bezos', 'Elon Musk', 'Sergey Brin', 'Larry Page', 'Steve Jobs', 'Bill Gates'];
moguls.forEach(function(mogul,placeInArray){
console.log(placeInArray+1 + ". Mr. " + mogul);
});
If you ran the above function in the console, you’d get:
- Mr. Jeff Bezos
- Mr. Elon Musk
- Mr. Sergey Brin
- Mr. Larry Page
- Mr. Steve Jobs
- Mr. Bill Gates
The function that’s being used as a parameter is the anonymous function:
function(mogul,placeInArray) {
console.log(placeInArray+1 + ". Mr. " + mogul);
});
forEach
is the higher-order function that’s operating on the above anonymous function.
We could have used the anonymous function on it’s own, like this:
var a = function(mogul, placeInArray) {
console.log(placeInArray+1 + ". Mr. " + mogul);
}
Next, we’d have called it like this:
a("Nobody",0);
Calling the a
function like that would print this out to the console:
1. Mr. Nobody
But, by passing the a
function (in it’s anonymous variety) to the forEach
function, we effectively give our a function super-powers! Now it can easily take on a full array of names, without too much effort on our side.
Alright, so let’s now see how to do that same thing in Elm.
Mapping Over a List in Elm
What follows are three variations on the above JS code, simplified and re-written in Elm:
module Main exposing (main)
import Html exposing (Html)
moguls = ["Jeff Bezos", "Elon Musk", "Sergey Brin", "Larry Page", "Steve Jobs", "Bill Gates"]
addTitle name = String.append "Mr. " name
main : Html msg
main =
Html.text <| Debug.toString <| List.map (\x -> addTitle x) moguls
Alternatively, we could have written it like this:
module Main exposing (main)
import Html exposing (Html)
moguls = ["Jeff Bezos", "Elon Musk", "Sergey Brin", "Larry Page", "Steve Jobs", "Bill Gates"]
addMrToAnyString string =
"Mr. " ++ string
main : Html msg
main =
Html.text <| Debug.toString <| List.map addMrToAnyString moguls
Yet another way to write this would be:
module Main exposing (main)
import Html exposing (Html)
moguls = ["Jeff Bezos", "Elon Musk", "Sergey Brin", "Larry Page", "Steve Jobs", "Bill Gates"]
addMrToAnyString = (++) "Mr. "
main : Html msg
main =
Html.text <| Debug.toString <| List.map addMrToAnyString moguls
In the above example, the addMrToAnyString
is built using partial application of the append operator ++
.
This is possible because:
- all operators in Elm are functions
- all functions in Elm are curried, i.e. can be partially applied
The expression (++) "Mr. "
is applying a single argument to the function (++)
, thus producing a new function for the second argument.
How List.map is used
Looking at the examples above, we always see this pattern:
List.map aFunction aList
The List.map
function takes two arguments, the first one being a function, and the second one being the actual list to be mapped over.
Adding an Ordinal Number in Front of Each Member of the List
In this example, we’ll look at combining List.map
with List.indexedMap
.
Since List.map
takes a function and a List
and returns a List
, it is entirely possible to then run List.indexedMap
on the List
that got returned:
module Main exposing (main)
import Html exposing (Html)
moguls : List String
moguls =
[ "Jeff Bezos", "Elon Musk", "Sergey Brin", "Larry Page", "Steve Jobs", "Bill Gates" ]
addMrToAnyString : String -> String
addMrToAnyString =
(++) "Mr. "
main : Html msg
main =
Html.text <|
Debug.toString <|
-- List.indexedMap (,) <|
List.indexedMap Tuple.pair <|
List.map addMrToAnyString moguls
The above code, when run, will produce a List
of 2-tuples
, with the first member in each of the 2-tuples
, a number
. Like this:
[(0,"Mr. Jeff Bezos"),(1,"Elon Musk"),...,(5,"Mr. Bill Gates")]
Here is a slight improvement over the code above, that produces nicer results:
module Main exposing (main)
import Html exposing (..)
moguls : List { name : String }
moguls =
[ { name = "Jeff Bezos" }
, { name = "Elon Musk" }
, { name = "Sergey Brin" }
, { name = "Larry Page" }
, { name = "Steve Jobs" }
, { name = "Bill Gates" }
]
addMrToAnyString =
(++) "Mr. "
printMogul mogul =
li [] [ text (addMrToAnyString mogul.name)
]
mogulsFormatted =
div [] [ h1 [] [ text "Moguls" ]
, ol [] (List.map printMogul moguls)
]
main : Html msg
main =
mogulsFormatted
And the output is:
Moguls
1. Mr. Jeff Bezos
2. Mr. Elon Musk
3. Mr. Sergey Brin
4. Mr. Larry Page
5. Mr. Steve Jobs
6. Mr. Bill Gates
Revisiting our FizzBuzz app
We can now use List.map to make a better FizzBuzz app:
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
ourList = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
fizzBuzzCheck fizz buzz fizzBuzz num =
if modBy 15 num == 0 then
Debug.toString fizzBuzz ++ ", "
else if modBy 5 num == 0 then
Debug.toString buzz ++ ", "
else if modBy 3 num == 0 then
Debug.toString fizz ++ ", "
else
(Debug.toString num) ++ ", "
main =
text (String.concat (List.map (fizzBuzzCheck "fizz" "buzz" "fizz buzz") ourList ) )
The code of the above example can be found on Ellie app.
Before we start discussing what’s going on in the preceding code, let’s quickly update the main function using the forward function application operator, |>
:
module Main exposing (main)
import HTML exposing (HTML, text)
ourList = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
fizzBuzzCheck fizz buzz fizzBuzz num =
if num % 15 == 0 then
toString fizzBuzz ++ ", "
else if num % 5 == 0 then
toString buzz ++ ", "
else if num % 3 == 0 then
toString fizz ++ ", "
else
(toString num) ++ ", "
main =
List.map (fizzBuzzCheck "fizz" "buzz" "fizz buzz") ourList
|> String.concat
|> text
Seeing the main
function written in this different notation might make it simpler to understand what is happening in the preceding code. After importing the Main
and Html
modules, we declare the ourList
variable and the fizzBuzzCheck
function definition.
As we can see, the fizzBuzzCheck
function takes four parameters and returns a value of type String
.
The main
function maps ourList
based on the logic in the fizzBuzzCheck
function, then we use the String.concat
function to take that List
of Strings
that the List.map
produced, and turn it into a single String
, because the text
function receives a single String
value as its parameter.
This concludes our discussion of List.map
and List.filter
for now.
In the next article, we’ll look into currying and partial application, which will help us understand function signatures in Elm 0.19.