Maps: records with names
A map holds values under names, which makes it the record of an Elixir program. Maps are values, so changing one gives back a new map and leaves the old one alone.
Values under names
A map is written with braces and pairs: %{name: "Ada", age: 36, city: "London"}. The key is the atom before the colon, and the value is what follows. Two ways ask for something back: user.name when you are sure the key is there, and user[:name] which answers nil when it is not. Map.get(user, :key, "fallback") is the third, and the one to use when a missing key is normal. A map is a value like any other, so it can be a function argument, an element of a list, or the answer of a function; nothing about it is tied to a name.
Any value can be a key — strings, numbers, even tuples — but atoms are the convention for records, because writing user.name is shorter and reads like a field. The set of key names is the shape of the record, and it is the part you will match on when you take records apart.
Change makes a new map
There is no way to reach into a map and alter it. Map.put(user, :age, 37) gives back a new map with that one pair changed, and the map you passed in is exactly as it was. A name bound before the call still holds the old value afterwards, which is why a program can be followed line by line — nothing behind your back is changing.
For the common case of replacing a key that is already there, Elixir has a shorter update form: %{user | age: 37}. It insists that the key exists and raises if it does not, which is exactly right for a record: a typo in a field name becomes an error, not a second field. Map.update(user, :visits, 1, fn n -> n + 1 end) is the version that copes with a key that might be missing — the third argument is the value to start with when it is absent.
Counting with a map
Maps are how you count things. Walk a list with Enum.reduce, carrying an empty map, and use Map.update to add one to the count for each item: a key that is missing appears with a count of 1, and one that is there goes up by one. That five-line pattern is so common that the language ships it as a single call, Enum.group_by, which returns a map from the computed key to the list of items that produced it. The map that comes out is a value like any other, so it can be sorted, printed, or handed to another function without conversion.
One thing to leave at the door: a map has no order to rely on when you print it. When a report needs to read in a particular sequence, sort it — Enum.sort_by(groups, fn {town, _orders} -> town end) — rather than hoping the map will come out your way.
A record, changed and counted
book = %{title: "Dune", year: 1965, pages: 412}
IO.puts(book.title)
IO.inspect(book[:year])
IO.inspect(Map.get(book, :pages))
IO.inspect(Map.get(book, :author, "unknown"))
reprint = Map.put(book, :year, 1966)
IO.inspect(reprint.year)
IO.inspect(book.year)
IO.inspect(Map.update(reprint, :year, 1, fn y -> y + 1 end))
IO.inspect(Map.has_key?(book, :title))
IO.inspect(Enum.sort(Map.keys(book)))
counts =
Enum.reduce(["x", "y", "x"], %{}, fn letter, acc ->
Map.update(acc, letter, 1, fn n -> n + 1 end)
end)
IO.inspect(counts)
IO.inspect(counts["x"])A person, changed without being touched
Run it and check that the original map still says 36 after the copy says 37. Then add a field of your own, raise the count of one letter, and take two keys out with Map.take.
Choosing between the two ways to read a key is choosing what happens when it is missing. The dot raises, so the mistake shows up where you made it. Map.get with a fallback keeps going, which is right for data from outside the program, where an absent field is normal.
Keys are compared exactly
A map compares keys the way === does, so 1 and 1.0 are different keys even though == calls them equal. Strings and atoms are different too: %{"name" => "Ada"} does not answer to map.name, because that dot asks for the atom :name. Pick one kind of key for a record and stay with it. The dot form is shorthand for an atom key, so map.name and map[:name] ask for the same key, while map["name"] asks for a different one.
Small maps and large maps are stored differently, and the runtime may switch representation as a map grows. The language hides that, but it is why the printed order is not part of the value. When order matters, convert the pairs with Map.to_list and sort them.
map.missing raises KeyError; use Map.get when a missing key is normal. %{map | key: value} only replaces an existing key and raises otherwise, while Map.put adds it. And a string key is not an atom key: map[:name] will not find %{"name" => "Ada"}.
Try these next
- Build a map with three atom keys, read one with the dot and one with
Map.get. - Change one key with
Map.putand check that the original still holds the old value. - Count the letters in a word with
Enum.reduceandMap.update.
A small inventory
Run it and check the stock before and after the changes. Then add a fourth item, set one count to zero with `Map.put`, and print the items that are out of stock.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does Map.put(user, :age, 37) leave in the variable user?
