Quickstart Elm 0.19, part 6

List.map and List.filter in Elm

By: Ajdin Imsirovic 24 October 2019

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.

A close-up of a race track with an Elm logo overlaid

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:

  1. Mr. Jeff Bezos
  2. Mr. Elon Musk
  3. Mr. Sergey Brin
  4. Mr. Larry Page
  5. Mr. Steve Jobs
  6. 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.

Feel free to check out my work here: