DEV Community

Mark
Mark

Posted on • Edited on • Originally published at alchemist.camp

Elixir maps made effortless

Getting used to working with Elixir maps can be one of the most painful aspects of really getting comfortable with the language. If you're coming from a language like Java or Ruby, the fact that everything is immutable can be frustrating to deal with. If you're coming from JavaScript, you'll have that problem and be spoiled by having native maps (Objects, in JS speak) fit perfectly with JSON.

The good news is, there are really only a couple of points that trip people up!

Variables are immutable in Elixir

// JavaScript
a = {foo: 42}
b = a
b.foo   // equals 42
b.foo = 5
a   // equals {foo: 5}
# Elixir
a = %{foo: 42}
b = a
b.foo   # equals 42
b.foo = 5   # b and b.foo are immutable so we get an error
# ** (CompileError) iex:5: cannot invoke remote function b.foo/0 inside a match

b = %{b| foo: 5}
b   # now b is reassigned and bound to %{foo: 5}
a.foo   # still equals 42

All "updating" of maps involves reassigning a variable to a new map

Here are a few common ways:

  • put adds a new value to a map: a = Map.put(a, bar: 5) Now a is %{foo: 42, bar: 5}
  • delete removes a value from a map: a = Map.delete(a, :foo) Now a is %{bar: 5}
  • put_new works like put, but does nothing if the key already exists: a = Map.put_new(a, :bar, 10) Doesn't replace the existing key, so a is still %{bar: 5}
  • merge adds/updates multiple values into a map: a = Map.merge(a, %{foo: "stuff", baz: -5}) Now a is %{foo: "stuff", bar: 5, baz: -5}

Note that since Elixir variables are immutable, the map functions above created new maps instead of changing a itself. Without reassigning a to the new maps with the a = at the front of each of the examples above, a would be unchanged.

Map keys can be Strings or Atoms (or anything!)

In Elixir, you'll run into two forms of map keys. Some, like the examples above, are atoms and some are strings. Very rarely, you may find integers, floats or even other types, including nested maps used as keys of maps!

The following are all valid maps:

  • a = %{:some_atom => :foo}
  • i = %{1 => 52}
  • f = %{1.5 => i} - the value of f is now: %{1.5 => %{1 => 52}}
  • m = %{f => "wat???"} - the value of m is now: %{%{1.5 => %{1 => 52}} => "wat???"}

Strings are the same thing as Erlang binaries.

  • They're represented with quotes. "foo"
  • Longer strings or strings with quotation marks in them can be made with sigils ~s{I'm a string made from a sigil and can have "quoted" parts}
  • In a key-value form, string keys use an arrow syntax. %{"foo" => "stuff", "bar" => 5}
  • When being used to access values, they use a bracket syntax. a["foo"] is "stuff" and b["foo"] is 5

Atoms are unique symbols. They don't get garbage collected so don't generate them dynamically.

  • When represented alone, they start with a colon. :foo
  • In a key-value form, they can use the standard arrow syntax or simply have a trailing colon. %{foo: "stuff, bar: 5} is the same as %{:foo => "stuff", :bar => 5}
  • When being used to access values, they can use a dot syntax. a.foo is the same as a[:foo]

Working with deeply nested maps

The above techniques are enough to do anything with Elixir maps, however immutability makes working with deeply nested maps a bit cumbersome. Intermediate steps would be required to build up the exact structure you want to reassign a given variable to.

Of course you're always free to write your own helpers, but for the 90% case, the built-in get_in, put_in and related functions will do the job. They're part of Kernel, not Map, because they also operate on other types of data, so they don't have a Map. prefix.

users = %{
  "sam" => %{age: 22},
  "pat" => %{age: 58}
}

get_in(users, ["sam", :age])
# returns 22

put_in(users["pat"][:age], 28)
# returns %{"sam" => "{age: 22}, "pat" => %{age: 28}}

Request a free email-based Elixir course from Alchemist.Camp

Top comments (0)