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