← All series

Series contents

Log in to track your progress

  1. 1. Welcome to Alchemist Camp Episode
  2. 2. Lesson 1: The guessing game Episode
  3. 3. Solution 1: The Greeter Episode
  4. 4. Lesson 2: Word Count Episode
  5. 5. Word Count ULTIMATE EDITION! Episode
  6. 6. Minimal Todo List Part 1 Episode
  7. 7. Elixir lists vs Elixir tuples Article
  8. 8. The Image Snatcher Episode
  9. 9. Minimal Todo List Part 2 Episode
  10. 10. Elixir maps made effortless Article
  11. 11. Fibonacci with tail recursion Episode
  12. 12. Mini Markdown Episode
  13. 13. Extending Mini Markdown Episode
  14. 14. Alchemy Markdown Episode
  15. 15. Using mix to modularize and add tests Episode
  16. 16. Command line applications with escript Episode
  17. 17. Caboose, a simple Unix tail clone Episode
  18. 18. Game board (Tictac Part 1) Episode
  19. 19. Structs: Maps with checks and default values Article
  20. 20. Deck generation, Pythagorean Triplets and more Episode
  21. 21. Whiteboarding the Tictac state machine Episode
  22. 22. Game state machine (Tictac Part 2) Episode
  23. 23. Modelling the memory game Episode
  24. 24. Finishing Tictac and making a CLI Episode
  25. 25. Threatened by rooks, bishops and knights Episode
  26. 26. A worker to log stats from a YouTube’s API Episode
  27. 27. Constructing bitmap files with Elixir Episode
  28. 28. Making bitmaps with color palettes Episode
  29. 29. Flattening lists recursively in Elixir Episode

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

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}}

Open standalone page ↗

4 Comments

@Krambits: glad it was helpful! I think this bit of sytactic sugar is confusing to most people who didn't come from Ruby (which has something very similar with its maps).

- alchemist · 5 years ago

Thank you. Now i understand why % [alchemist: "elixirSuperhero"] Is the same as %[:alchemist => "elixirSuperhero"] It confused me a lot when I was reading Phoenix source code.

- Krambits · 5 years ago

I've replaced the Disqus comments here with a hand-rolled solution. The reason was purely UX.

The vast majority of the page loading time and bandwidth for users to load an article was due to Disqus as opposed to the actual content of the articles (or even the content of the comments themselves). This is definitely a WIP, but things should only get better from here!

- alchemist · 6 years ago

Thank you for article.

- Neon23 · 2 years ago