Elixir maps made effortless
Mark
Posted on July 9, 2019
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" andb["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 asa[: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
Posted on July 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.