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
Actually they can be just about anything! But you'll run into two forms of Elixir map keys all the time—Strings and Atoms. Very rarely, you may find integers, floats or even other types, including nested maps used as keys of maps, too.
The following are all valid maps:
a = %{:some_atom => :foo}
i = %{1 => 52}
f = %{1.5 => i}
- f is now: %{1.5 => %{1 => 52}}
m = %{f => "wat???"}
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.
a = %{"foo" => "stuff", "bar" => 5}
- When being used to access values, they use a bracket syntax.
a["foo"]
is "stuff" and a["bar"]
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. These two forms are identical:
%{foo: "stuff", bar: 5}
%{:foo => "stuff", :bar => 5}
- When being used to access values, they can use a dot syntax.
a.foo
is the same as a[:foo]
The %{key => value}
syntax is the "main" syntax. It's flexible, it allows keys to be passed in from variables and it just works.
The dot syntax is a convenience and it only works when the key is an atom.
It's a bit similar to JavaScript's two ways of getting the color value out of this object: jsObj = {color: "red"};
We can get it with jsObj.color
or with jsObj["color"]
but only the second form would work if the key were being passed in via a variable
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}}