Nested Dictionary Structures

The various dictionary types let us associate keys with values. But those values can themselves be dictionaries. For example, we may have a bug-reporting system. We could represent this using the following:

maps/nested.exs
 defmodule​ Customer ​do
  defstruct ​name:​ ​"​​"​, ​company:​ ​"​​"
 end
 
 defmodule​ BugReport ​do
  defstruct ​owner:​ %Customer{}, ​details:​ ​"​​"​, ​severity:​ 1
 end

Let’s create a simple report:

 iex>​ report = %BugReport{​owner:​ %Customer{​name:​ ​"​​Dave"​, ​company:​ ​"​​Pragmatic"​},
 ...>​ ​details:​ ​"​​broken"​}
 %BugReport{details: "broken", severity: 1,
  owner: %Customer{company: "Pragmatic", name: "Dave"}}

The owner attribute of the report is itself a Customer struct.

We can access nested fields using regular dot notation:

 iex>​ report.owner.company
 "Pragmatic"

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}

Ugly stuff! We had to update the overall bug report’s owner attribute with an updated customer structure. This is verbose, hard to read, and error prone.

Fortunately, Elixir has a set of nested dictionary-access functions. One of these, put_in, lets us set a value in a nested structure:

 iex>​ put_in(report.owner.company, ​"​​PragProg"​)
 %BugReport{details: "broken",
  owner: %Customer{company: "PragProg", name: "Dave"},
  severity: 1}

This isn’t magic—it’s simply a macro that generates the long-winded code we’d have to have written otherwise.

The update_in function lets us apply a function to a value in a structure.

 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. The documentation in IEx contains everything you need for these. However, 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:

These are a natural consequence of the way the macros bake their parameters into code at compile time.

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.

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)

Here’s a simple example:

maps/dynamic_nested.exs
 nested = %{
 buttercup:​ %{
 actor:​ %{
 first:​ ​"​​Robin"​,
 last:​ ​"​​Wright"
  },
 role:​ ​"​​princess"
  },
 westley:​ %{
 actor:​ %{
 first:​ ​"​​Cary"​,
 last:​ ​"​​Elwes"​ ​# 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: "Cary", last: "Elwes"}, role: "farm boy"}}

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.

maps/get_in_func.exs
 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" ]

The Access Module

The Access module provides a number of predefined functions to use as parameters to get and get_and_update_in. These functions act as simple filters while traversing the structures.

The all and at functions only work on lists. all returns all elements in the list, while at returns the nth element (counting from zero).

maps/access1.exs
 cast = [
  %{
 character:​ ​"​​Buttercup"​,
 actor:​ %{
 first:​ ​"​​Robin"​,
 last:​ ​"​​Wright"
  },
 role:​ ​"​​princess"
  },
  %{
 character:​ ​"​​Westley"​,
 actor:​ %{
 first:​ ​"​​Cary"​,
 last:​ ​"​​Elwes"
  },
 role:​ ​"​​farm boy"
  }
 ]
 
 IO.inspect get_in(cast, [Access.all(), ​:character​])
 #=> ["Buttercup", "Westley"]
 
 IO.inspect get_in(cast, [Access.at(1), ​:role​])
 #=> "farm boy"
 
 IO.inspect get_and_update_in(cast, [Access.all(), ​:actor​, ​:last​],
 fn​ (val) -> {val, String.upcase(val)} ​end​)
 #=> {["Wright", "Elwes"],
 # [%{actor: %{first: "Robin", last: "WRIGHT"}, character: "Buttercup",
 # role: "princess"},
 # %{actor: %{first: "Cary", last: "ELWES"}, character: "Westley",
 # role: "farm boy"}]}

The elem function works on tuples:

maps/access2.exs
 cast = [
  %{
 character:​ ​"​​Buttercup"​,
 actor:​ {​"​​Robin"​, ​"​​Wright"​},
 role:​ ​"​​princess"
  },
  %{
 character:​ ​"​​Westley"​,
 actor:​ {​"​​Carey"​, ​"​​Elwes"​},
 role:​ ​"​​farm boy"
  }
 ]
 
 IO.inspect get_in(cast, [Access.all(), ​:actor​, Access.elem(1)])
 #=> ["Wright", "Elwes"]
 
 IO.inspect get_and_update_in(cast, [Access.all(), ​:actor​, Access.elem(1)],
 fn​ (val) -> {val, String.reverse(val)} ​end​)
 #=> {["Wright", "Elwes"],
 # [%{actor: {"Robin", "thgirW"}, character: "Buttercup", role: "princess"},
 # %{actor: {"Carey", "sewlE"}, character: "Westley", role: "farm boy"}]}

The key and key! functions work on dictionary types (maps and structs):

maps/access3.exs
 cast = %{
 buttercup:​ %{
 actor:​ {​"​​Robin"​, ​"​​Wright"​},
 role:​ ​"​​princess"
  },
 westley:​ %{
 actor:​ {​"​​Carey"​, ​"​​Elwes"​},
 role:​ ​"​​farm boy"
  }
 }
 
 IO.inspect get_in(cast, [Access.key(​:westley​), ​:actor​, Access.elem(1)])
 #=> "Elwes"
 
 IO.inspect get_and_update_in(cast, [Access.key(​:buttercup​), ​:role​],
 fn​ (val) -> {val, ​"​​Queen"​} ​end​)
 #=> {"princess",
 # %{buttercup: %{actor: {"Robin", "Wright"}, role: "Queen"},
 # westley: %{actor: {"Carey", "Elwes"}, role: "farm boy"}}}

Finally, Access.pop lets you remove the entry with a given key from a map or keyword list. It returns a tuple containing the value associated with the key and the updated container. nil is returned for the value if the key isn’t in the container.

The name has nothing to do with the pop stack operation.

 iex>​ Access.pop(%{​name:​ ​"​​Elixir"​, ​creator:​ ​"​​Valim"​}, ​:name​)
 {"Elixir", %{creator: "Valim"}}
 iex>​ Access.pop([​name:​ ​"​​Elixir"​, ​creator:​ ​"​​Valim"​], ​:name​)
 {"Elixir", [creator: "Valim"]}
 iex>​ Access.pop(%{​name:​ ​"​​Elixir"​, ​creator:​ ​"​​Valim"​}, ​:year​)
 {nil, %{creator: "Valim", name: "Elixir"}}