Joy of Elixir

9. Built-in functions: lists, maps and more

Working with lists

Just like there is a String module for working with strings, there is also a List module for working with lists. This module includes functions like first that will tell you what the first item in a list is:

List.first([1, 2, 3, 4])
1

And it also lets you figure out what the last item is too:

List.last([1, 2, 3, 4])
4

We looked earlier at how we could reverse a string, but how about if we wanted to reverse something else, like this list?

iex> animals_or_derivatives_of_animals = ["cat", "dog", "cow", "turducken"]

We would think that just like first and last that there should be a reverse too! Let's try it out:

iex> List.reverse(animals_or_derivatives_of_animals)
List.reverse(animals_or_derivatives_of_animals)
** (UndefinedFunctionError) function List.reverse/1 is undefined or private
    (elixir) List.reverse(["cat", "dog", "cow", "turducken"])

Uh oh, there's that red text again. It seems like that this function doesn't exist, even though we thought it did. We are lacking the superpower of being able to will functions into being at this current point in time, so we'll have to do some sleuthing to figure out why it's missing.

The computer is telling us that Elixir doesn't know about a function called List.reverse, or the function is "private". The computer (slyly) won't tell us which one of non-existence or privateness it is, but we'll assume the first case here: that the function is undefined. (We'll come back to private functions a little later on in the book.)

Ok, so we've talked about what /1 means (Roberto is now deep in thought), so let's talk about why List.reverse/1 doesn't exist.

Thank you Reader, but our princess function is in another castle module

The short version of why the List.reverse/1 function doesn't exist is because it lives in a separate module; it exists in the Enum module. The name Enum is short for Enumerable, and it's done that way because nobody has the time to write out Enumerable correctly every single time. Even as this book's author I have a hard time spelling it correctly each time! Thank goodness for autocomplete.

"What on earth is an enumerable?", Roberto cries out. Hold your horses, Roberto. We're getting to that.

Lists are a type of data in Elixir called an enumerable. Maps are also enumerables. This means that they can be enumerated through; which means that you can do something which each item in the enumerable object (a list or a map). For instance, if we were to write out our list on a piece of paper, it might look something like this:

  • Cat
  • Dog
  • Cow
  • Turducken

It's possible to write each item from the list separately from the other items in the list. Because we're able to do this we can safely say that this list is enumerable. We could try to do the same thing for a number (like 1,354), but it wouldn't make sense:

  • 1
  • 3
  • 5
  • 4

Numbers are not enumerable because it doesn't make sense for them to be written like this, unlike our list.

Similar to this, we could enumerate through a map. If we were to take one of our maps from earlier...

%{"name" => "Roberto", "age" => "30ish", "gender" => "Male"}

...and write each key and value pair down, they may look something like this:

Name
Roberto
Age
30ish
Gender
Male

Again, it makes sense for a map to be an enumerable because you can enumerate through each of the key and value pairs in the map.

And now for the big reveal

We need to keep in mind that when we're working with enumerable things in Elixir the function that we need might live elsewhere. Sometimes, we need to look in the Enum module / drawer too.

We tried looking in the List module to find the reverse/1 function so that we could turn our list around but it wasn't there. We found out a short while ago that the function is actually within the Enum module, so let's try using that function. Before that, let's get our list in Elixir form again. It's been a while since we've seen it that way:

iex> animals_or_derivatives_of_animals = ["cat", "dog", "cow", "turducken"]

Since we now know that lists are enumerables, and that the List.reverse/1 function doesn't exist but we also (now) know that there's an Enum module to work with this sort of thing, we can probably guess that there's going to be an Enum.reverse/1 function. Let's try it and see:

iex> Enum.reverse(animals_or_derivatives_of_animals)
  ["turducken", "cow", "dog", "cat"]

Hooray! We were able to reverse our list.

Roberto's features relax from intense concentration to a more neutral setting and he asks, "Hey, you mentioned before you could enumerate through a list or a map, but you didn't show an example of that. What gives?" You're absolutely right, Roberto. I was too distracted with explaining why List.reverse/1 didn't exist to explain how to enumerate through an enumerable.

Let's all now take a look at how to do that before we move onto other functions. We've talked about enums briefly and it would be a shame to stop so early when you can do so much more with them than reverse them.

Enumerating the enumerables

To appease Roberto (and the masses that he leads) yet again, we're going to need to look at how to enumerate through enumerables. What this means is that we're going to get an enumerable (a list or a map) and we're going to go through each of the enumerable's items and do something with them.

Let's start with a list. How about a list of the seasons, just for something different?

iex> seasons = ["summer", "fall", "winter", "spring"]

The simplest way of enumerating through a list in Elixir is to use the Enum.each function. This function takes two arguments: the thing we want to enumerate through and a function to run for each of the items. Let's take a look at an example:

iex> Enum.each(seasons, &IO.puts/1)

The first argument here is our list of seasons. The second argument is the function that we want to run for each item in this list: specifically the built-in IO.puts/1 function. As we saw in the last chapter, IO.puts/1 will output a string to the terminal during a program's execution.

Let's take a look at what this combination of Enum.each, seasons and &IO.puts/1 does:

iex> Enum.each(seasons, &IO.puts/1)
summer
fall
winter
spring
:ok

This particular combination of those three things has made Elixir output each season on a single line in our iex prompt. Then there's one more thing: that little blue :ok at the end of our output. This indicates to us that the iterating with Enum.each/2 worked successfully.

Let's take a closer look at what this is doing. Specifically: what &IO.puts/1 is doing. You know in Elixir that functions can take arguments and you might have in your head that those arguments could be strings, lists or maps becuase this is what we've seen done previously. But function arguments can also be other functions. This is what's happening here when we're using &IO.puts/1. We're passing that function as an argument to Enum.each/2.

To get an idea of how Enum.each/2 is able to take the IO.puts/1 function and do something with it, we can use this code in the iex prompt:

iex> puts = &IO.puts/1
puts.("summer")
"summer"
:ok

With this code, we've assigned the IO.puts/1 function to a variable called puts. This is similar to what's going on inside the Enum.each/2 function: whatever function we pass as the second argument will be stored with a particular name. Then Enum.each/2 goes through each of the items and calls the provided function on it, just like we've done above.

"That last line looks exactly like how we ran functions back in Chapter 5", Roberto exclaims. Yes, Roberto! That is exactly right. Back in that chapter we defined a greeting function like this:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

We then made Elixir run this function using this syntax:

iex> greeting.("World")
"Hello, World!"

What we're doing different here with our puts = &IO.puts/1 is that we're using an inbuilt function rather than constructing one of our own. We can do this with any inbuilt function we please. Here's another example, using String.capitalize/1:

iex> cap = &String.capitalize/1
cap.("summer")
"Summer"

Here, the String.capitalize/1 function takes a string and turns the first letter into a capital letter. In this code example, we're making it so that we can call the String.capitalize/1 as if it were a function called cap. However, if we wanted to capitalise the names of our season using the String.capitalize/1 function with Enum.each/2 it wouldn't do very much:

iex> Enum.each(seasons, &String.capitalize/1)
:ok

Nothing is output here besides :ok because we're not telling Elixir to output anything. It will dutifully run the function String.capitalize/1 for each item in the list, and then just as dutifully "forget" to tell us anything about the result.

If we want to see what the result would be from evaluating String.capitalize/1 for each item in the list, we're going to have to use a different function.

Map, but not the kind that you know already

The names of these seasons should be capitalized because they are nouns, but whoever created this list neglected to capitalize them. Oops! What we should have is a list with proper capitalization:

["Summer", "Fall", "Winter", "Spring"]

We tried using Enum.each/2 to give us this list of capitalized seasons, but that just told us :ok and it didn't give us back a list of capitalized season names.

We're going to need a function that goes through each item in the list and applies String.capitalize/1, and then shows us the result of each application of that function. Something that would turn our existing seasons list into a list like this one:

iex> seasons = [
    String.capitalize("summer"),
    String.capitalize("fall"),
    String.capitalize("winter"),
    String.capitalize("spring"),
  ]
  ["Summer", "Fall", "Winter", "Spring"]

But since we've already got a list we're going to have to do something to that list to turn it into what we want. That something is to use another function from the Enum module called Enum.map/2. This map function is different from the map kind of data (%{"name" => "Roberto"}), in that the function-called-map will take the specified enumerable and another function as arguments, then enumerate through each item in the list and run that other function for each item, and then return the result of each function run.

Enough jibber-jabber. Let's see this in actual practice:

iex> Enum.map(seasons, &String.capitalize/1)
["Summer", "Fall", "Winter", "Spring"]

Hooray! Each of our seasons now has a correctly capitalized name. What's happened here is that we've told the map function that we want it to run another function -- that'd be the String.capitalize/1 function -- on each item of the seasons list. This is how we've been able to go from our existing list with non-capitalized season names to a new list with capitalized names.

BYO functions

One more quick example. When using Enum.each/2 or Enum.map/2 we don't necessarily need to pass a built-in function as the second argument. We could choose to pass our own function instead. Let's say that we had a list of numbers like this:

iex> numbers = [4, 8, 15, 16, 23, 42]

And then we wanted to multiply each number by a different number, let's say 42. We're lazy and we want the computer to do the heavy lifting, and so we can pass Enum.map/2 a function of our own design to acheive this goal:

iex> Enum.map(numbers, fn (number) -> number * 2 end)
[8, 16, 30, 32, 46, 84]

With our own function here, we're taking each element of the list -- represented inside the function as the variable number -- and then multiplying it by 42. When Enum.map/2 has finished going through the list, it outputs a new list showing us the result of running that function on all items in the list.

Reducing an enumerable

Cover reduce and ranges?

Working with maps

Now that we've looked at enumerating through lists and ranges, let's look at enumerating through the other kind of enumerable that we've seen: maps. We spoke a little while ago about doing so:

Similar to this, we could enumerate through a map. If we were to take one of our maps from earlier...

%{"name" => "Roberto", "age" => "30ish", "gender" => "Male"}

...and write each key and value pair down, they may look something like this:

Name
Roberto
Age
30ish
Gender
Male

To enumerate through each item in the map we can use the same function as when we were enumerating through each item in a list: Enum.each/2. Here's an example:

iex> person = %{"name" => "Roberto", "age" => "30ish", "gender" => "Male"}
%{"name" => "Roberto", "age" => "30ish", "gender" => "Male"}
iex> Enum.each(person, fn ({key, value}) -> IO.puts value end)

Before I can go any further, Roberto interrupts. "Hey, those curly braces are weird! It looks like a map but it's not a map because it doesn't have the % before it. And it's not a list because the curliness of the brackets; a list has square brackets. So what is it?"

The {k, v} here indicates a type of data in Elixir that we haven't seen before called a tuple. Tuples work like lists in that the order of the elements inside them matters, but you can't enumerate through them like you can with lists; said another way, tuples are not enumerable.

In this function passed to each, a tuple is used instead of two function arguments (i.e. fn(key, value) ->) to indicate that the key and the value share a connection. This key and value are not represented as a list (i.e. fn([key, value]) ->) because of this connection.

Let's take another look at how we used Enum.each/2 and what the output was:

iex> Enum.each(person, fn ({key, value}) -> IO.puts value end)
30ish
Male
Roberto
:ok

Our use of Enum.each/2 goes through each key and value, ignores the key and outputs the value with IO.puts/1. This is almost identical to our previous example of Enum.each/2, where we used a list of seasons with the IO.puts/1 function:

iex> Enum.each(seasons, &IO.puts/1)

In the list example, we were able to use &IO.puts/1. In the map example though, we had to pull out the value by using the tuple, and then we needed to use the expanded version of IO.puts/1 (IO.puts(value)) to output that value.

Where to find more functions

Talk about the h helper in the iex prompt. Talk about tab complete here too? TODO: Link to https://hexdocs.pm/elixir/

Exercises