Joy of Elixir

10. Working with maps

In the last chapter we looked at some functions that we could use to work with lists. We looked at some functions from the `List` module, but we really spent most of the chapter on the `Enum` module. In this chapter, we'll spend some time talking about how to use `Enum.each/2` and `Enum.map/2` on maps instead of lists. Then we'll cover some functions from the `Map` module.

We spoke in that last chapter of using the `Enum.each/2` function to work with maps:

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

``%{name: "Izzy", age: "30ish", gender: "Female"}``

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

Name
Izzy
Age
30ish
Gender
Female

But we never got around to actually showing you how this was done! Well, enough dilly-dallying, here's an example:

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

Before I can go any further, Izzy interrupts. "Hey, those curly braces inside the function passed to `each` 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?". Izzy is calling out this code specifically:

``fn ({key, value}) ...``

The `{key, value}` 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. Tuples are used to represent ordered, linked data. In this example, each tuple represents a key and its linked value.

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. (Also: Elixir in its own "brain" thinks of maps as a list of tuples, and so this is another reason why they're tuples, and not lists!)

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
Female
Izzy
: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 cities with the `IO.puts/1` function:

``iex> Enum.each(cities, &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. Let's look at what happens if we don't use that expanded version.

``````iex> Enum.each(person, &IO.puts/1)
** (Protocol.UndefinedError) protocol String.Chars not implemented for {"age", "30ish"}``````

Uh oh, the computer is mad at us again. This cryptic error message is a complicated way of saying that `IO.puts/1` doesn't know how to output a tuple. We just need to be careful that we pass `IO.puts/1` only things it can handle. If we give something that `IO.puts/1` can't handle, it won't know how to output it, and so it will show this error.

We've now looked at `Enum.each/2` for maps, just like how we looked at it in the last chapter for lists. In that last chapter we also looked at `Enum.map/2` and `Enum.reduce/2` and so it's only fair that we cover them here too.

Mapping over maps

I wonder what map we could use this time. We've used the `person` map quite a few times already:

``iex> person = %{name: "Izzy", age: "30ish", gender: "Female"}``

We should try to use another map for this section. Hmmmm. How about a map with the days of the week and their expected temperatures? Here's what the week ahead looks like in Melbourne at this current point of writing:

``````%{
"Monday" => 28,
"Tuesday" => 29,
"Wednesday" => 29,
"Thursday" => 24,
"Friday" => 16,
"Saturday" => 16,
"Sunday" => 20
}``````

These expected temperatures aren't just random numbers. That's just Melbourne for you. It just can't make up its mind. Now what if for our American friends we represented these numbers in their familiar — but wacky — farenheit? Otherwise they might read the numbers and think that Melbourne is a frigid hellscape, which it most certainly is not.

So we want to convert these temperatures from their celsius amounts into the farenheit equivalents, turning the map into this:

``````%{
"Monday" => 82.4,
"Tuesday" => 84.2,
"Wednesday" => 84.2,
"Thursday" => 75.2,
"Friday" => 60.8,
"Saturday" => 60.8,
"Sunday" => 68
}``````

We know already how to convert these numbers if it was simply a list of those numbers, without the days of the week, like this:

``[28, 29, 29, 24, 16, 16, 20]``

We would use `Enum.map/2` to run a function on each of these numbers to convert the numbers to farenheit (using that function that we saw back in Chapter 5:

``````iex> forecast = [28, 29, 29, 24, 16, 16, 20]
[28, 29, 29, 24, 16, 16, 20]
iex> Enum.map(forecast, fn (temp) -> temp * 1.8 + 32 end)
[82.4, 84.2, 84.2, 75.2, 60.8, 60.8, 68.0]``````

So what if we just tried using `Enum.map/2` on our forecast map instead of on a list? What would that look like? Let's see:

``````iex> forecast = %{
"Monday" => 28,
"Tuesday" => 29,
"Wednesday" => 29,
"Thursday" => 24,
"Friday" => 16,
"Saturday" => 16,
"Sunday" => 20
}
%{"Friday" => 16, ...}
iex> Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
[
{"Friday", 60.8},
{"Monday", 82.4},
{"Saturday", 60.8},
{"Sunday", 68.0},
{"Thursday", 75.2},
{"Tuesday", 84.2},
{"Wednesday", 84.2}
]``````

"Hey, that's no map! That's a list!", Izzy exclaims, seemingly adapting a quote from a relatively niche space opera series. That's right, Izzy. When we call `Enum.map/2` on either a list or a map we're going to get back a list regardless. This has to do with how Elixir thinks of maps: it thinks of them as a list of key-value tuples, rather than the friendly `%{}` syntax that we know and love. To get this data back into the format that we're familiar with, we're going to need to use another function from the `Enum` toolbox, called `Enum.into/2`.

Let's run our `Enum.map/2` line again, but this time we'll assign the output to a variable.

``````iex> new_forecast = Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
[
{"Friday", 60.8},
{"Monday", 82.4},
{"Saturday", 60.8},
{"Sunday", 68.0},
{"Thursday", 75.2},
{"Tuesday", 84.2},
{"Wednesday", 84.2}
]``````

Then we can use `Enum.into/2` to convert this list of tuples back into a map:

``````iex> Enum.into(new_forecast, %{})
%{"Friday" => 60.8, "Monday" => 82.4, "Saturday" => 60.8, "Sunday" => 68.0,
"Thursday" => 75.2, "Tuesday" => 84.2, "Wednesday" => 84.2}``````

That's better! Our data is back into the shape of a map... but the days of the week have lost their ordering. But that's okay because we don't care about ordering in maps: we access values in our maps by their matching key, not by their position.

So here we've seen how to use the `Enum.map/2` and `Enum.into/2` functions to iterate through a map's elements to run a function on each of those elements.

However, there is a cleaner way of writing the above code and that's with the help of one of the best Elixir features: the pipe operator. Let's spend a little while looking at the pipe operator now.

The wonderful pipe operator

I've just shown you one way to use `Enum.map/2` to go over a map's values and to apply a function to each of those values. Here it is again, in case you missed it:

``````iex> forecast = %{
"Monday" => 28,
"Tuesday" => 29,
"Wednesday" => 29,
"Thursday" => 24,
"Friday" => 16,
"Saturday" => 16,
"Sunday" => 20
}
%{"Friday" => 16, ...}
iex> new_forecast = Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
[
{"Friday", 60.8},
{"Monday", 82.4},
{"Saturday", 60.8},
{"Sunday", 68.0},
{"Thursday", 75.2},
{"Tuesday", 84.2},
{"Wednesday", 84.2}
]
iex> Enum.into(new_forecast, %{})
%{
"Friday" => 60.8,
"Monday" => 82.4,
"Saturday" => 60.8,
"Sunday" => 68.0,
"Thursday" => 75.2,
"Tuesday" => 84.2,
"Wednesday" => 84.2
}``````

What we're doing here can be broken down into a few steps:

1. We define a variable called `forecast`, which is a map containing the days of the week and their expected temperatures.
2. We then take this `forecast` variable, and pass it as the first argument to `Enum.map/2`. The second argument passed to `Enum.map/2` is a function which runs on each key-value pair in the map. This function takes the temperature -- the value -- and converts it to farenheit.
3. We assign the result of this `Enum.map/2` call to a variable called `new_forecast`, and then immediately pass that into `Enum.into/2` as the first argument. The second argument given to `Enum.into/2` is an empty map. This converts the output of `Enum.map/2` -- a list of key-value tuples -- into a friendly map.

Elixir has a way of writing this code in a shorter way that I would like to show you now as I believe it can lead to shorter, cleaner Elixir code. We might find more uses for it as we go along too, and so this seems like a pretty good time to introduce this particular concept.

The way to write shorter code here is by using the pipe operator, `|>`. The pipe operator allows us to chain together actions within Elixir, passing the result of one function on to the next. It's called the pipe operator because it connects the functions together in a linear fashion, with data flowing from one function to the next; like water through a pipe. Unfortunately, unlike Tony Hawk's Pro Skater, we do not get thousands of points for chaining together sick combos. But we still do get the great sense of satisfaction when it all works together!

Let's look at a small example of the pipe operator using functions from the `String` module first before we use it on this forecast data.

``````iex> "hello pipe operator" |> String.upcase() |> String.reverse()
"ROTAREPO EPIP OLLEH"``````

We can think of the pipe operator as "and then". In the above example, we:

• Have a string
• and then we call `String.upcase()`
• and then we call `String.reverse()`

Here's an illustrated version:

This code takes the string, and then passes it to `String.upcase()`. The result of that operation is `"HELLO PIPE OPERATOR"`. Then, we take that uppercased string and pass it to `String.reverse()`, which will then turn it into `"ROTAREPO EPIP OLLEH"`.

Without the pipe operator, we would have to write this code as:

``iex> String.reverse(String.upcase("hello pipe operator"))``

This code is harder to read and understand, because we have to read it from the inside out. With the pipe operator, we read the code as we read this sentence: from left to right. We can more easily understand the transformations our data goes through by using the pipe operator in Elixir.

Let's now use this pipe operator on our forecast data. Here's some code that uses the pipe operator ("and then") that is functionally identical to the code from the previous section, where we have a forecast in celsius and we convert it to farenheit:

``````%{
"Monday" => 28,
"Tuesday" => 29,
"Wednesday" => 29,
"Thursday" => 24,
"Friday" => 16,
"Saturday" => 16,
"Sunday" => 20
}
|> Enum.map(fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
|> Enum.into(%{})
|> IO.inspect
``````

Go ahead and put this code in a file called `forecast.exs` and run it with `elixir forecast.exs`. This is what you'll see as the output:

``````%{
"Friday" => 60.8,
"Monday" => 82.4,
"Saturday" => 60.8,
"Sunday" => 68.0,
"Thursday" => 75.2,
"Tuesday" => 84.2,
"Wednesday" => 84.2
}``````

I've put each key and value onto its own line, but you can see here that the output is the same as before in our `iex` console, and the code is neater to boot! "Amazing", utters Izzy. So what's happened here? Let's run through it:

1. We define a map containing the days of the week and their expected temperatures. We don't assign this to a variable.
2. And then we pipe this map using the pipe operator (`|>`) into `Enum.map/2`. This makes the map the first argument of `Enum.map/2`, so this removes the need to define a `forecast` variable and to pass it in as the first argument. The pipe operator gives the map to the function automatically. The only argument on `Enum.map/2` here is just the function that we use to convert the temperature values. We still refer to this function as `Enum.map/2`, because it is taking in two arguments: the piped-in value and the function.
3. And then we pipe the result of this `Enum.map/2` call into `Enum.into/2`, and the result of `Enum.map/2` becomes the first argument of `Enum.into/2`. The second argument to `Enum.into/2` is still an empty map.
4. Finally, we pipe the result of `Enum.into/2` into a function we've not seen before called `IO.inspect/1`. This outputs the map in a human-friendly way. We're not using `IO.puts/1` here because it only works with strings, and what we have at this point of the code after `Enum.into/2` is a map, not a string. `IO.inspect/1` will work with any form of data that we give it.

"Wow, that's a lot of text!", says Izzy, looking a little stunned. "Do you have an image that I could look at instead?". Sure I do!

You can think of the pipe operator not only as an "and then" operator, but also like the stages of a waterfall -- or "datafall" -- with data flowing from one function to the next.

Each operation that we're doing in `forecast.exs` passes its result onto the next line, in a pipeline of operations. Elixir allows us to chain these operations together seamlessly with this pipe operator. Everything just flows from one line to the next until we reach the end. There is no need for storing data into temporary variables like `new_forecast`; you just simply pipe the output from one function to the next.

So that's the wonderful pipe operator! How handy!

We started this chapter by making some promises, though. We've already fulfilled one promise—we've talked about the `Enum` module's functions `each/2` and `map/2`. But we also promised to talk about some functions from the `Map` module. Let's get into it. We might even find another excuse to use the pipe operator.

The marvellous Map module and its fantastic contraptions

Rightio, so we've spent a lot of the last few pages looking at two modules: the `List` and `Enum` module. It's time for a breath of fresh air -- time to break out of our `Enum` comfort zone that we've wiggled into and time to get uncomfortable again! Let's look at a brand-new-to-us module: the `Map` module.

If you want to do nearly anything with maps that doesn't involve enumerating over them, you would use this `Map` module. Let's look at a few common actions that you might take on a map.

Finding a key's value

In Chapter 4: Marvellous Maps we saw that we could fetch the value of a particular key by using square brackets.

``````iex> person = %{"gender" => "Female", "age" => "30ish", "name" => "Izzy"}
%{"age" => "30ish", "gender" => "Izzy", "name" => "Izzy"}
iex> person["name"]
"Izzy"``````

We compared it to a skilltester reaching in to pluck out the relevant item:

The `Map` module provides us another way to get that value out: `Map.get/2`.

``iex> Map.get(person, "name")``

Now this might not seem very useful. After all, why not just use the old fashioned way: `person["name"]`? Well, one place where this might come in handy is if you want to find a particular key in the middle of a pipe chain. Imagine you have a variable called `get_attendees` that gives you data that looks like this:

``````%{
"people" => ["Izzy", "The Author"],
"robots" => ["Roboto", "TARS"]
}``````

Then from this data you wanted to get the first person from the list of people. You could write this as:

``````
attendees = get_attendees()
List.first(attendees["people"])
``````

But then this code is confusing. The order looks like:

1. Get list of attendees
2. Get the first from that list...
3. That list of `attendees["people"]`

Both Elixir and our brains need to interpret what's inside the brackets of `List.first/1` first, then interpret `List.first/1`. That requires us to jump around a little in the code because the code is not executed in the order that you would read it.

If we were to use the pipe operator (`|>`) and `Map.get/2`, we could really clean this code up and make it read a lot better. Here we go:

``get_attendees() |> Map.get("people") |> List.first``

That's much neater and it reads in the exact order of operations:

1. Get the attendees
2. Then, get the value that the `"people"` key points to
3. Then, run `List.first/1` on that value

So `Map.get/2` has its uses when pipe chaining. However, if you just wanted to access a value in a map regularly, I'd still say stick with code like `people["name"]`.

I want to show you three more functions from `Map` which I think are as important to know as `Map.get/2` and those are `Map.put/3`, `Map.merge/2` and `Map.delete/2`.

put

The `Map.put/3` function takes three arguments: a map, a key and a value. It will then return an updated version of the original map. How that map gets updated depends on what the key was: if it was an existing key, only the value for that key is updated. If it's a new key, then both the key and value are added. Let's look at some examples to get a clearer understanding.

You might want to use this function if you've got a map containing data that you want to update. For instance, like the data that we have on Izzy so far:

``iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}``

Recently, we've found out that Izzy is from Australia. That would explain the accent and the cork hat. We've also found out that Izzy is "40ish" rather than "30ish". This is surprising to us, considering her youthful visage. Based on this new information, we'll add a new key to our map called `country` and update the `age` key with the more correct value. We could just type it all out again:

``````iex> izzy = %{
"name" => "Izzy",
"age" => "40ish",
"gender" => "Female",
"country" => "Australia",
}``````

But that seems like an awful lot of typing just to update one key. Let's look at how we could update the `age` key and add in the `country` key by using `Map.put/3`:

``````iex> izzy = Map.put(izzy, "age", "40ish")
%{"age" => "40ish", "gender" => "Female", "name" => "Izzy"}``````

Ah that's so much better than writing the whole map from scratch! We can now update the `"age"` key without having to rewrite the whole map.

Now let's say that we wanted to update this `age` key and the `country` key at the same time. We can use `Map.put/2` and the pipe operator to chain the two operations together:

``````izzy |> Map.put("age", "40ish") |> Map.put("country", "Australia")
%{"age" => "40ish", "country" => "Australia", "gender" => "Female", "name" => "Izzy"}``````

This time, we've updated the value under the `age` key, and we've also added a new key here called `country`. It's important to note here that the original `izzy` map isn't changed at all:

``````izzy
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}``````

This is because of that immutability thing we talked about a few chapters ago. Data in Elixir never changes unless we re-assign the variable it is associated with. In the above `Map.put/3` code, `izzy` stays the same, even though the functions run. If we wanted to update the `izzy` variable with new data, we could re-assign that variable:

``````izzy = izzy |> Map.put("age", "40ish") |> Map.put("country", "Australia")
%{"age" => "40ish", "country" => "Australia", "gender" => "Female", "name" => "Izzy"}``````

From that point onwards, our code would know that Izzy is 40ish and from Australia. Phew.

How we've used `Map.put/3` here is another great example of the pipe operator in action, but we can write the code in a shorter fashion with the next `Map` function: `merge/2`.

merge

Let's change the data that we have on Izzy back to what it was at the start of this chapter:

``izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}``

This'll make it easier to show what `Map.merge/2` does. `Map.merge/2` takes two maps and merges them together. Let's take our last `Map.put/3` example and rewrite it with `Map.merge/2` to demonstrate this:

``````izzy = Map.merge(izzy, %{"age" => "40ish", "country" => "Australia"})
%{
"age" => "40ish",
"country" => "Australia",
"gender" => "Female",
"name" => "Izzy"
}``````

This results in the same map as our `Map.put/3` example, except we only have to call one function once instead of twice. The `Map.merge/2` function takes two maps as arguments, and then:

• Takes the keys from the second map that are not present in the first map adds them to the first map
• Overwrites keys in the first map that are present in the second map

In this example, it has replaced the "old" `age` value ("30ish") with the "new" one ("40ish"), and it has added a key called `country` to this map.

Pipe Merging

While we can use `Map.merge/2` to combine two maps to update an existing map, there is also a shorter way, which we'll call pipe merging. The syntax goes like this:

``````iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
iex> %{izzy | "age" => "40ish", "name" => "Isadora"}
%{"age" => "40ish", "gender" => "Female", "name" => "Isadora"}``````

This syntax looks like a map, but it's split in two. On the left hand side, inside the map syntax, we have a variable which represents an existing map. On the right hand side, we have keys we would like to update in that map, along with their new values. As we can see, the map's `"age"` and `"name"` keys change, taking on their values from the right hand side.

However, it's important to note that if we try to add a new key to this map, it will not work with this syntax:

``````iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
iex> %{izzy | "country" => "Australia"}

** (KeyError) key "country" not found in: %{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
(stdlib) :maps.update("country", "Australia", %{"age" => "30ish", "gender" => "Female", "name" => "Izzy"})``````

This pipe merging feature will only work with existing keys in the map, and this is an important difference to notice from `Map.merge/2`, which will work with both kinds of keys: new and pre-existing.

delete

We've now looked at how to add new keys or update keys in a map. The only thing left to look at is how to delete them. We can delete keys in a map by using the `Map.delete/2` function. Let's say we didn't really care what country Izzy came from. We can delete that data, but keep everything else, by using `Map.delete/2`

``````izzy = Map.delete(izzy, "country")
%{"age" => "40ish", "gender" => "Female", "name" => "Izzy"}
``````

This function takes a map as its first argument, and then a key as its second argument. When the function acts, it removes that key from the map.

Summary

In this chapter, we've seen a couple of ways of working with maps. We saw that we could go through each key-value pair in the map with `Enum.each/2`. We also used `Enum.map/2` and `Enum.into/2` to change the values inside the maps to other values, converting temperature numbers to farenheit ones.

When we looked at `Enum.map/2` and `Enum.into/2`, we discovered that we could write the code in a shorter way with the pipe operator: `|>`. This operator allows us to pass the result of one function call to the next, allowing us to write cleaner Elixir code.

We rounded out the chapter by looking at some common functions from the `Map` module.

We saw `Map.get/2` which allowed us to pick out a particular value based on a key's name. We knew we could already do this with code like `person["name"]`, but, as we saw, `Map.get/2` is useful if we're in the middle of a piping operation and need to pick out a value from a map.

We then looked at `Map.put/3` which gave us the ability to create a new map with new keys from an existing map. This function took a map, a key and a value and then gave us back a new map with the changes we asked for.

If we were applying multiple changes with `Map.put/3` at the same time, we learned that it wasn't smart to do that and so we then learned about `Map.merge/2`. We saw that this function takes two maps and merges them together, with the 2nd maps keys taking precedence over the first map's keys.

The last thing we saw in this chapter was how to get back a map without certain keys with `Map.delete/2`. It's a good way of removing data from a map if we don't need it anymore.

We're now moving out of the `Enum`, `List` and `Map` woods into something brand new to us: using Elixir to interact with files on our computer.

Exercises

• Use your newfound knowledge of the pipe operator to re-write your solution to Chapter 8's first exercise.