Powered by AppSignal & Oban Pro

Maps

reading/maps.livemd

Maps

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.8.0", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively, you can evaluate the Elixir cells as you read.

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • What is a map? How do they differ from keyword lists?
  • How do we create a map?
  • How do we use dot notation and square bracket notation to access values in a map?
  • How do we pattern match on maps?

Maps

Maps are another type of associative data structure. You can create a map using %{}. Maps can use almost any elixir term as a key. However, most commonly we use strings and atoms for map keys.

Maps have a key and a value separated by an arrow =>, which is an equals symbol = and a less than symbol >.

# string keys
%{"key" => "value"}

# atom keys
%{:key => "value"}

multiple key-value pairs are separated by a comma ,.

%{:key => "value", "key" => "value"}

Maps with only atoms as keys can use a special piece of syntax sugar that makes them more convenient to write and read. This is purely syntax sugar and does not change their behavior.

%{key1: "value", key2: "value"}

Maps are the common go-to for a key-value data structure. They are more expensive in terms of performance to create than keyword lists, however they are incredibly fast to access values. Unlike keyword lists, keys in a map must be unique. Also unlike keyword lists, maps also do not guarantee key order.

For these reasons, you’ll most often use maps for large amounts of data or when you want keys to be unique and do not care about order.

We often use maps to represent properties of something. For example, you might have a user map that represents information about a book.

%{
  title: "Name of the Wind",
  author: "Patrick Rothfuss",
  description: """
  The Name of the Wind is a fantasy novel by Patrick Rothfuss. It is the first book in the
  Kingkiller Chronicle series and tells the story of Kvothe, a young orphan who becomes a
  legendarily talented musician and magician. The novel is known for its intricate
  world-building, complex characters, and themes of power, love, and loss.
  """  
}

Elixir is very helpful and provides a warning to let us know we’re overriding a duplicate key.

%{duplicate_key: "value1", duplicate_key: "value2"}

As mentioned earlier, maps do not guarantee key order, which is why you’ll notice the returned value of the map below does not have the same order as the map returned by the Elixir cell.

%{one: "one", two: "two", three: "three"}

Maps can have any elixir term as a key, this makes them incredibly flexible.

%{1 => "value"}

%{[1, 2, 3] => "value"}

Accessing Map Values

Unlike the other data types, there aren’t specific map operators. To manipulate maps we use a different tool called the Map module, which you will learn more about in a future lesson.

For now, it’s enough to know that you can access values in an atom-key map using a few different methods.

You can retrieve values in a map using map.key notation like so. This only works for maps with atom keys.

%{key: "value"}.key

Alternatively, you can access the map value using square bracket notation map[key].

%{key: "value"}[:key]

Bracket notation is especially useful for maps with non-atom keys.

%{"key" => "value"}["key"]
%{1 => "value"}[1]

With map.key notation, your program will throw an error if your map doesn’t have the expected value.

%{}.key

Bracket notation will return nil if the key doesn’t exist rather than throwing an error.

%{}[:key]

We can access deeply nested values with both dot notation and bracket notation.

%{key1: %{key2: %{key3: "value"}}}.key1.key2.key3
%{1 => %{2 => %{3 => "value"}}}[1][2][3]

Bracket notation is especially useful for accessing deeply nested values without causing a crash if the value doesn’t exist.

%{}[1][2][3]

You can accomplish all of the same behavior above by binding the map to a variable rather than accessing values from the map directly.

map = %{key: "value"}
map.key
map = %{key: "value"}
map[:key]

Your Turn

In the Elixir cell below, access the :hello key in map. Retrieve the value using both map[key] and map.key notation.

Example solution

map.key notation.

map = %{hello: "world"}
map.hello

map[] notation.

map = %{hello: "world"}
map[:hello]
map = %{hello: "world"}

Updating Maps

You can update values in a map using %{initial_map | updated_values} syntax like so.

initial = %{key: "value"}

%{initial | key: "new value"}

Elixir does not allow you to mutate values. That means the variable initial is still %{key: "value"}

initial

You can instead store a new variable for the updated map.

updated = %{initial | key: "new value"}
updated

Or rebind the existing initial variable.

initial = %{initial | key: "new value"}
initial

You can only update existing atom key values in a map, otherwise it will cause an error.

initial = %{}
%{initial | new_key: "value"}

Your Turn

Use a map to represent a todo item The todo map should have :title and :completed keys.

  • title: “finish maps exercises”, completed: false

Bind your map to a todo variable, and update the value so completed is true.

Example solution

todo = %{title: "finish maps exercise", completed: false}

%{todo | completed: true}

Pattern Matching

We can pattern match on values inside the map.

%{hello: my_variable} = %{hello: "world"}
my_variable

Unlike with keyword lists, we don’t have to match on every key/value pair when pattern matching with maps.

%{one: one} = %{one: 1, two: 2}
one

This also works with non-atom key maps.

%{"hello" => world} = %{"hello" => "world"}

We can only pattern match on the value in the map, not the key. Keys in maps must be literals such as atoms, strings, tuples, etc.

%{hello => world} = %{"hello" => "world"}

Pin Operator

You’ll notice that we can rebind variables using pattern matching.

name = "Jon"

%{name: name} = %{name: "Bill"}

name

If instead of re-binding the variable, we want to use the literal value of the variable, we can use the pin operator ^.

name = "Jon"

%{name: ^name} = %{name: "Bill"}

name

The above causes a MatchError because the left side and right side do not match. It’s the same as if we had written the following.

%{name: "Jon"} = %{name: "Bill"}

We might use this to confirm our map matches some shape.

Your Turn

Bind 2 and 4 in the following map to variables two and four.

Example solution

%{two: two, four: four} = %{one: 1, two: 2, three: 3, four: 4}

Enter your solution below.

%{one: 1, two: 2, three: 3, four: 4}

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Mark As Completed

file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")

save_name =
  case Path.basename(__DIR__) do
    "reading" -> "maps_reading"
    "exercises" -> "maps_exercise"
  end

progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()

default = Map.get(existing_progress, save_name, false)

form =
  Kino.Control.form(
    [
      completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
    ],
    report_changes: true
  )

Task.async(fn ->
  for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
    File.write!(
      progress_path,
      Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
    )
  end
end)

form

Commit Your Progress

Run the following in your command line from the curriculum folder to track and save your progress in a Git commit. Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.

$ git checkout -b maps-reading
$ git add .
$ git commit -m "finish maps reading"
$ git push origin maps-reading

Create a pull request from your maps-reading branch to your solutions branch. Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.

DockYard Academy Students Only:

Notify your teacher by including @BrooklinJazz in your PR description to get feedback. You (or your teacher) may merge your PR into your solutions branch after review.

If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.

Up Next

Previous Next
Keyword Lists Shopping List