Elixir 1.3
Chapter 8
Maps, Keyword Lists,
Sets, and Structs
How to Choose Between Maps
and Keyword Lists
Ask yourself these questions (in this order):
- Do I want to pattern-match against the contents (matching a dictionary that has a key of :name somewhere in it)?
- use a map
- Will I want more than one entry with the same key?
- use the Keyword module.
- Do I need to guarantee the elements are ordered?
- use the Keyword module.
- And, if you’ve reached this point:
- Use a map
Keyword Lists
defmodule Canvas do
@defaults [ fg: "black", bg: "white", font: "Merriweather" ]
def draw_text(text, options \\ []) do
options = Keyword.merge(@defaults, options)
IO.puts "Drawing text #{inspect(text)}"
IO.puts "Foreground: #{options[:fg]}"
IO.puts "Background: #{Keyword.get(options, :bg)}"
IO.puts "Font: #{Keyword.get(options, :font)}"
IO.puts "Pattern: #{Keyword.get(options, :pattern, "solid")}"
IO.puts "Style: #{inspect Keyword.get_values(options, :style)}"
end
end
Canvas.draw_text("hello", fg: "red", style: "italic", style: "bold")
# =>
# Drawing text "hello"
# Foreground: red
# Background: white
# Font: Merriweather
# Pattern: solid
# Style: ["italic", "bold"]
Keyword lists are typically used as
options passed to functions.
Maps
iex> map = %{ name: "Dave", likes: "Programming", where: "Dallas" }
%{likes: "Programming", name: "Dave", where: "Dallas"}
iex> Map.keys map
[:likes, :name, :where]
iex> Map.values map
["Programming", "Dave", "Dallas"]
iex> map[:name]
"Dave"
iex> map.name
"Dave"
iex> map1 = Map.drop map, [:where, :likes]
%{name: "Dave"}
iex> map2 = Map.put map, :also_likes, "Ruby"
%{also_likes: "Ruby", likes: "Programming", name: "Dave", where: "Dallas"}
iex> Map.keys map2
[:also_likes, :likes, :name, :where]
iex> Map.has_key? map1, :where
false
iex> { value, updated_map } = Map.pop map2, :also_likes
{"Ruby", %{likes: "Programming", name: "Dave", where: "Dallas"}}
iex> Map.equal? map, updated_map
true
Maps are the go-to key/value data structure in Elixir.
They have good performance at all sizes.
Pattern Matching and Updating Maps
person = %{ name: "Dave", height: 1.88 }
# Is there an entry with the key :name?
iex> %{ name: a_name } = person
%{height: 1.88, name: "Dave"}
iex> a_name
"Dave"
# Are there entries for the keys :name and :height?
iex> %{ name: _, height: _ } = person
%{height: 1.88, name: "Dave"}
# Does the entry with key :name have the value "Dave"?
iex> %{ name: "Dave" } = person
%{height: 1.88, name: "Dave"}
# Our map does not have the key :weight, so the following pattern match fails:
iex> %{ name: _, weight: _ } = person
** (MatchError) no match of right hand side value: %{height: 1.88, name: "Dave"}”
A common questions is?
“Do you have the following keys (and maybe values)?”
Pattern Matching and Updating Maps
people = [
%{ name: "Grumpy", height: 1.24 },
%{ name: "Dave", height: 1.88 },
%{ name: "Dopey", height: 1.32 },
%{ name: "Shaquille", height: 2.16 },
%{ name: "Sneezy", height: 1.28 }
]
IO.inspect(for person = %{ height: height } <- people, height > 1.5, do: person)
iex> [%{height: 1.88, name: "Dave"}, %{height: 2.16, name: "Shaquille"}]
Let's use destructuring and a for loop to iterate over a collection and filter it as we go.
We are feeding a list of maps (people) to our comprehension. We bind each map to person and binds the height from that map to height. The filter selects only those maps where the height exceeds 1.5,
and the do block prints the whole map.
Pattern Matching and Updating Maps
people = [
%{ name: "Grumpy", height: 1.24 },
%{ name: "Dave", height: 1.88 },
%{ name: "Dopey", height: 1.32 },
%{ name: "Shaquille", height: 2.16 },
%{ name: "Sneezy", height: 1.28 }
]
defmodule HotelRoom do
def book(%{name: name, height: height}) when height > 1.9 do
IO.puts "Need extra long bed for #{name}"
end
def book(%{name: name, height: height}) when height < 1.3 do
IO.puts "Need low shower controls for #{name}"
end
def book(person) do
IO.puts "Need regular bed for #{person.name}"
end
end
people |> Enum.each(&HotelRoom.book/1)
#=> Need low shower controls for Grumpy
# Need regular bed for Dave
# Need regular bed for Dopey
# Need extra long bed for Shaquille
# Need low shower controls for Sneezy
Pattern Matching Can't Bind Keys
iex> %{ 2 => state } = %{ 1 => :ok, 2 => :error }
%{1 => :ok, 2 => :error}
iex> state
:error
iex> %{ item => :ok } = %{ 1 => :ok, 2 => :error }
** (CompileError) iex:5: illegal use of variable item in map key…
Pattern Matching Can Match Variable Keys
iex> data = %{ name: "Dave", state: "TX", likes: "Elixir" }
%{likes: "Elixir", name: "Dave", state: "TX"}
iex> for key <- [ :name, :likes ] do
...> %{ ^key => value } = data
...> value
...> end
["Dave", "Elixir"]
Earlier we saw that the pin operator could be used to use the value already in a variable on the left-hand side of a match. We can do the same with the keys of a map.
Updating a Map
iex> m = %{ a: 1, b: 2, c: 3 }
%{a: 1, b: 2, c: 3}
iex> m1 = %{ m | b: "two", c: "three" }
%{a: 1, b: "two", c: "three"}
iex> m2 = %{ m1 | a: "one" }
%{a: "one", b: "two", c: "three"}
We can add a new key/value entry or replace an entry in a map without traversing the whole structure but the result is a new map, because immutability.
new_map = %{ old_map | key => value, … }
You have to use the Map.put_new/3 function
for adding a new key.
Structs
defmodule Subscriber do
defstruct name: "", paid: false, over_18: true
end
What if we want to create a typed map, a map that has a fixed set of fields and default values for those fields, and that you can pattern-match by type as well as content?
Enter the struct.
A struct is just a module that
wraps a limited form of map.
The name of the module becomes the name of the map type. Inside the module, you use the defstruct macro to define the map’s characteristics.
Structs
$ iex defstruct.exs
iex> s1 = %Subscriber{}
%Subscriber{name: "", over_18: true, paid: false}
iex> s2 = %Subscriber{ name: "Dave" }
%Subscriber{name: "Dave", over_18: true, paid: false}
iex> s3 = %Subscriber{ name: "Mary", paid: true }
%Subscriber{name: "Mary", over_18: true, paid: true}
# You access the fields in a struct using dot notation or pattern matching:
iex> s3.name
"Mary"
iex> %Subscriber{name: a_name} = s3
%Subscriber{name: "Mary", over_18: true, paid: true}
iex> a_name
"Mary"
# updates:
iex> s4 = %Subscriber{ s3 | name: "Marie"}
%Subscriber{name: "Marie", over_18: true, paid: true}
The syntax for creating a struct is the same as the syntax for creating a map—you simply add the module name between the % and the {.
Structs
defmodule Attendee do
defstruct name: "", paid: false, over_18: true
def may_attend_after_party(attendee = %Attendee{}) do
attendee.paid && attendee.over_18
end
def print_vip_badge(%Attendee{name: name}) when name != "" do
IO.puts "Very cheap badge for #{name}"
end
def print_vip_badge(%Attendee{}) do
raise "missing name for badge"
end
end
Why are structs wrapped in a module? The idea is that you are likely to want to add struct-specific behavior.
Structs
$ iex defstruct1.exs
iex> a1 = %Attendee{name: "Dave", over_18: true}
%Attendee{name: "Dave", over_18: true, paid: false}
iex> Attendee.may_attend_after_party(a1)
false
iex> a2 = %Attendee{a1 | paid: true}
%Attendee{name: "Dave", over_18: true, paid: true}
iex> Attendee.may_attend_after_party(a2)
true
iex> Attendee.print_vip_badge(a2)
Very cheap badge for Dave
:ok
iex> a3 = %Attendee{}
%Attendee{name: "", over_18: true, paid: false}
iex> Attendee.print_vip_badge(a3)
** (RuntimeError) missing name for badge
defstruct1.exs:13: Attendee.print_vip_badge/1
Nested Dictionary Structures
defmodule Customer do
defstruct name: "", company: ""
end
defmodule BugReport do
defstruct owner: %Customer{}, details: "", severity: 1
end
iex> report = %BugReport{owner: %Customer{name: "Dave", company: "Pragmatic"},
...> details: "broken"}
%BugReport{details: "broken", severity: 1,
owner: %Customer{company: "Pragmatic", name: "Dave"}}
What happens when the values in a dictionary are a dictionary themselves?
The owner attribute of the report is itself a
Customer struct.
Nested Dictionary Structures
iex> report.owner.company
"Pragmatic"
We can access nested fields using regular dot notation:
But now our customer complains the company name is incorrect—it should be PragProg. Let’s fix it:
iex> report = %BugReport{ report | owner:
...> %Customer{ report.owner | company: "PragProg" }}
%BugReport{details: "broken",
owner: %Customer{company: "PragProg", name: "Dave"},
severity: 1}
That's pretty ugly but maybe Elixir has something to help us.
Nested Dictionary Structures
put_in and update_in macros to the rescue!
iex> put_in(report.owner.company, "PragProg")
%BugReport{details: "broken",
owner: %Customer{company: "PragProg", name: "Dave"},
severity: 1}
iex> update_in(report.owner.name, &("Mr. " <> &1))
%BugReport{details: "broken",
owner: %Customer{company: "PragProg", name: "Mr. Dave"},
severity: 1}
The other two nested access functions are
get_in and get_and_update_in.
Both of these functions support a cool trick:
nested access.
Nested Accessors and Nonstructs
If you are using the nested accessor functions with maps or keyword lists, you can supply the keys as atoms
iex> report = %{ owner: %{ name: "Dave", company: "Pragmatic" }, severity: 1}
%{owner: %{company: "Pragmatic", name: "Dave"}, severity: 1}
iex> put_in(report[:owner][:company], "PragProg")
%{owner: %{company: "PragProg", name: "Dave"}, severity: 1}
iex> update_in(report[:owner][:name], &("Mr. " <> &1))
%{owner: %{company: "Pragmatic", name: "Mr. Dave"}, severity: 1}
Dynamic (Runtime) Nested Accessors
The nested accessors we’ve seen so far are macros—they operate at compile time. As a result, they have some limitations:
- The number of keys you pass a particular call is static.
- You can’t pass the set of keys as parameters between functions.
To overcome this, get_in, put_in, update_in, and get_and_update_in can all take a list of keys as a separate parameter. Adding this parameter changes them from macros to function calls, so they become dynamic.
Dynamic (Runtime) Nested Accessors
Macro | Function | |
---|---|---|
get_in | no | (dict, keys) |
put_in | (path, value) | (dict, keys, value) |
update_in | (path, fn) | (dict, keys, fn) |
get_and_update_in | (path, fn) | (dict, keys, fn) |
Dynamic (Runtime) Nested Accessors
nested = %{
buttercup: %{
actor: %{
first: "Robin",
last: "Wright"
},
role: "princess"
},
westley: %{
actor: %{
first: "Carey",
last: "Ewes" # typo!
},
role: "farm boy"
}
}
IO.inspect get_in(nested, [:buttercup])
# => %{actor: %{first: "Robin", last: "Wright"}, role: "princess"}
IO.inspect get_in(nested, [:buttercup, :actor])
# => %{first: "Robin", last: "Wright"}
IO.inspect get_in(nested, [:buttercup, :actor, :first])
# => "Robin"
IO.inspect put_in(nested, [:westley, :actor, :last], "Elwes")
# => %{buttercup: %{actor: %{first: "Robin", last: "Wright"}, role: "princess"},
# => westley: %{actor: %{first: "Carey", last: "Elwes"}, role: "farm boy"}}
Dynamic (Runtime) Nested Accessors
authors = [
%{ name: "José", language: "Elixir" },
%{ name: "Matz", language: "Ruby" },
%{ name: "Larry", language: "Perl" }
]
languages_with_an_r = fn (:get, collection, next_fn) ->
for row <- collection do
if String.contains?(row.language, "r") do
next_fn.(row)
end
end
end
IO.inspect get_in(authors, [languages_with_an_r, :name])
#=> [ "José", nil, "Larry" ]
There’s a cool trick that the dynamic versions of both get_in and get_and_update_in support—if you pass a function as a key, that function is invoked to return the corresponding values.
Sets
iex> set1 = Enum.into 1..5, MapSet.new
#MapSet<[1, 2, 3, 4, 5]>
iex> MapSet.member? set1, 3
true
iex> set2 = Enum.into 3..8, MapSet.new
#MapSet<[3, 4, 5, 6, 7, 8]>
iex> MapSet.union set1, set2
#MapSet<[7, 6, 4, 1, 8, 2, 3, 5]>
iex> MapSet.difference set1, set2
#MapSet<[1, 2]>
iex> MapSet.difference set2, set1
#MapSet<[6, 7, 8]>
iex> MapSet.intersection set1, set2
#MapSet<[3, 4, 5]>
There is currently just one implementation of sets, the MapSet.
With Great Power Comes Great Temptation
The dictionary types are clearly a powerful tool but you might also be tempted to abuse them.
Structs can associate functions with them in their module definitions which can feel like a class. At some point you might think, “Hey, this is a bit like a class definition.” And you’d be right. You can write something akin to object-oriented code using structs (or maps) and modules.
This is a bad idea because you’ll be mixing paradigms and diluting the benefits a functional approach gives you.
Stay pure, young coder. Stay pure.
Thank you!
Programming Elixir 1.3 Chapter 08
By Dustin McCraw
Programming Elixir 1.3 Chapter 08
- 951