Maps in Elixir
A map is Elixir's general-purpose key-value store, written %{key => value} or, for atom keys, the shorthand %{key: value}. Unlike keyword lists, map keys are unique — inserting a duplicate key overwrites the existing value — and keys can be any term: atoms, strings, tuples, or even other maps. Maps give average O(1) lookup, insertion, and update via an efficient hashed internal representation, which is why they are the default choice in Elixir for representing structured data such as a parsed JSON payload, configuration, or a user record.
Cricket analogy: A map is like a player's career stats card keyed by format — %{test: 52.3, odi: 45.1, t20: 38.9} — where you look up the T20 average directly rather than scanning a full stat sheet, the way ESPNcricinfo profiles Virat Kohli.
Creating and Updating Maps
You build a map literally with %{a: 1, b: 2}, or from a list of pairs with Map.new/1. Reading a value uses map.key for atom keys or map[key] for any key type, both returning nil if missing, or Map.fetch/2 which returns {:ok, value} / :error. Updating an existing key uses the update syntax %{map | key: value}, which is efficient because it reuses the map's internal structure and only fails if the key does not already exist — for adding brand-new keys you use Map.put/3 instead.
Cricket analogy: %{scorecard | runs: 245} mirrors updating a live scoreboard's runs total mid-innings without rebuilding the whole card, the way a stadium screen updates after every boundary.
Pattern Matching and Nested Access
Maps pattern match partially: %{name: name} = user matches as long as user has at least a :name key, ignoring any other keys present, which makes function heads like def greet(%{name: name}), do: ... a concise way to destructure only the fields you care about. For deeply nested data, get_in/2, put_in/3, and update_in/3 with access paths like [:user, :address, :city] let you read or update nested maps without manually threading through each level, which is far less error-prone than chained pattern matches.
Cricket analogy: Matching %{name: name} against a full player record mirrors a commentator caring only about the batter's name field from a much larger stats object mid-broadcast.
Maps vs. Structs
A struct, defined with defstruct inside a module, is a specialized map with a fixed set of keys, a compile-time-checked __struct__ field, and no default Enumerable or Access protocol implementation unless you opt in — this trade-off protects you from typos in key names and from accidentally treating structured domain data as a generic bag of keys. Choose a plain map for loosely shaped or dynamic data (decoded JSON, config from a file), and choose a struct when you have a fixed, well-known shape like %User{name: ..., email: ...} that benefits from compile-time guarantees and pattern-matching on the struct name itself.
Cricket analogy: A struct %Player{name: ..., role: ...} is like an official ICC player registration form with fixed fields, whereas a raw map is like a scribbled scouting note that can carry any extra observation.
user = %{name: "Ada", age: 30, roles: [:admin]}
user.name # "Ada"
Map.get(user, :email, "none") # "none" - default when missing
Map.fetch(user, :age) # {:ok, 30}
updated = %{user | age: 31} # key must already exist
with_email = Map.put(user, :email, "ada@example.com")
%{name: name, roles: roles} = user
IO.puts("#{name} has roles: #{inspect(roles)}")
nested = %{profile: %{address: %{city: "Lagos"}}}
get_in(nested, [:profile, :address, :city]) # "Lagos"
put_in(nested, [:profile, :address, :city], "Nairobi")The update syntax %{map | key: value} raises a KeyError if key does not already exist in map — it can only update, never add. Use Map.put/3 or Map.put_new/3 when the key might be missing.
Map key lookup, insertion, and deletion are average-case O(1) thanks to Elixir's persistent hash array mapped trie (HAMT) representation under the hood, making maps the right default for most key-value data, not just an alternative to keyword lists.
- Maps store unique keys of any type with %{key => value} or %{key: value} shorthand for atoms.
- map.key and map[key] read values; Map.fetch/2 returns {:ok, value} or :error.
- %{map | key: value} updates an existing key efficiently but raises if the key is absent.
- Map.put/3 adds or overwrites a key; Map.put_new/3 only adds if absent.
- Partial pattern matching like %{name: name} = user destructures only the keys you need.
- get_in/2, put_in/3, and update_in/3 simplify working with nested map structures.
- Structs are fixed-shape maps defined with defstruct, trading flexibility for compile-time safety.
Practice what you learned
1. What happens if you use %{map | key: value} and key does not exist in map?
2. What is the average time complexity of Map.get/2 on an Elixir map?
3. What does Map.fetch(map, :missing_key) return when the key is absent?
4. Why would you choose a struct over a plain map?
5. What does %{name: name} = user match against?
Was this page helpful?
You May Also Like
Keyword Lists
Learn Elixir's keyword list — an ordered list of atom-keyed tuples used heavily for options, configuration, and DSL-style function calls.
Protocols in Elixir
Learn how Elixir protocols provide polymorphism across data types, letting a single function like Enum.map or to_string dispatch differently for lists, maps, and your own structs.
Lists and Tuples in Elixir
Learn how Elixir's two core sequential data structures — singly linked lists and fixed-size tuples — are represented in memory and when to use each one.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics