Elixir Lists and Maps with Cheatsheets
In Elixir, Lists and Maps are your primary data structures. Understanding their performance characteristics and idiomatic usage patterns is crucial for writing efficient, readable code.
Lists: Linked Lists Done Right
Elixir lists are immutable linked lists. This design choice has profound implications for both performance and programming patterns.
List Fundamentals
# Creating lists
empty_list = []
numbers = [1, 2, 3, 4]
mixed = [1, "two", :three, %{four: 4}]
# Head and tail (fundamental operations)
[head | tail] = [1, 2, 3, 4]
# head = 1, tail = [2, 3, 4]
# Prepending (O(1) - very fast)
new_list = [0 | numbers] # [0, 1, 2, 3, 4]
# Appending (O(n) - avoid if possible)
slow_append = numbers ++ [5] # [1, 2, 3, 4, 5]
Performance Characteristics
| Operation | Time Complexity | Example |
|---|---|---|
| Prepend | O(1) | [x \| list] |
| Access head | O(1) | [h \| _] = list |
| Access tail | O(1) | [_ \| t] = list |
| Append | O(n) | list ++ [x] |
| Length | O(n) | length(list) |
| Access by index | O(n) | Enum.at(list, index) |
Idiomatic List Patterns
defmodule ListExamples do
# Building lists efficiently (prepend, then reverse)
def build_range(0, acc), do: Enum.reverse(acc)
def build_range(n, acc), do: build_range(n - 1, [n | acc])
# Pattern matching on lists
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
# Processing lists with accumulator
def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([head | tail], acc), do: reverse(tail, [head | acc])
# List comprehensions
def squares(list) do
for n <- list, is_integer(n), do: n * n
end
# Filtering and transforming
def process_numbers(list) do
list
|> Enum.filter(&is_integer/1)
|> Enum.map(&(&1 * 2))
|> Enum.take(10)
end
end
Lists Cheatsheet
# Creation
[] # Empty list
[1, 2, 3] # Literal list
Enum.to_list(1..5) # [1, 2, 3, 4, 5]
# Basic operations
[h | t] = [1, 2, 3] # h=1, t=[2,3]
[0 | [1, 2, 3]] # [0, 1, 2, 3] - prepend
[1, 2] ++ [3, 4] # [1, 2, 3, 4] - concatenate
[1, 2, 3] -- [2] # [1, 3] - subtract
# Enum module (most common)
Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6]
Enum.filter([1, 2, 3, 4], &(&1 > 2)) # [3, 4]
Enum.reduce([1, 2, 3], 0, &+/2) # 6
Enum.find([1, 2, 3], &(&1 > 2)) # 3
Enum.all?([2, 4, 6], &(&1 > 0)) # true
Enum.any?([1, 2, 3], &(&1 > 2)) # true
# List module (list-specific)
List.first([1, 2, 3]) # 1
List.last([1, 2, 3]) # 3
List.flatten([[1, 2], [3, 4]]) # [1, 2, 3, 4]
List.zip([[1, 2], [3, 4]]) # [{1, 3}, {2, 4}]
# Comprehensions
for x <- [1, 2, 3], do: x * 2 # [2, 4, 6]
for x <- [1, 2, 3], x > 1, do: x # [2, 3]
Maps: Key-Value Excellence
Maps are Elixir’s primary associative data structure. They’re efficient, flexible, and essential for modeling real-world data.
Map Fundamentals
# Creating maps
empty_map = %{}
user = %{name: "Alice", age: 30, email: "[email protected]"}
string_keys = %{"name" => "Bob", "age" => 25}
mixed_keys = %{:atom_key => "value", "string_key" => :value}
# Accessing values
name = user.name # "Alice" (atom keys only)
name = user[:name] # "Alice" (safe access)
age = Map.get(user, :age) # 30
city = Map.get(user, :city, "Unknown") # "Unknown" (default)
Map Performance
| Operation | Time Complexity | Example |
|---|---|---|
| Access | O(log n) | map[:key] |
| Insert | O(log n) | Map.put(map, key, value) |
| Update | O(log n) | %{map \| key: new_value} |
| Delete | O(log n) | Map.delete(map, key) |
| Size | O(1) | map_size(map) |
Map Update Patterns
defmodule MapExamples do
# Updating existing keys
def update_user(user, new_age) do
%{user | age: new_age} # Fails if :age key doesn't exist
end
# Adding or updating keys
def set_user_city(user, city) do
Map.put(user, :city, city)
end
# Conditional updates
def increment_counter(map, key) do
Map.update(map, key, 1, &(&1 + 1))
end
# Pattern matching on maps
def process_user(%{name: name, role: :admin} = user) do
IO.puts("Processing admin: #{name}")
user
end
def process_user(%{name: name} = user) do
IO.puts("Processing user: #{name}")
user
end
# Nested map updates (Elixir 1.6.0+ syntax)
def update_address_city(user, new_city) do
put_in(user, [:address, :city], new_city)
end
# Safe nested access
def get_user_city(user) do
get_in(user, [:address, :city])
end
end
Maps Cheatsheet
# Creation
%{} # Empty map
%{a: 1, b: 2} # Atom keys
%{"a" => 1, "b" => 2} # String keys
Enum.into([{:a, 1}, {:b, 2}], %{}) # From list
# Access
map[:key] # Safe access (returns nil if missing)
map.key # Unsafe access (atom keys only)
Map.get(map, :key, default) # Access with default
Map.fetch(map, :key) # {:ok, value} or :error
Map.fetch!(map, :key) # value or KeyError
# Update
%{map | key: new_value} # Update existing key
Map.put(map, :key, value) # Put new or existing key
Map.update(map, :key, default, fun) # Update with function
Map.merge(map1, map2) # Merge maps
# Manipulation
Map.delete(map, :key) # Remove key
Map.drop(map, [:key1, :key2]) # Remove multiple keys
Map.take(map, [:key1, :key2]) # Keep only specified keys
Map.keys(map) # Get all keys
Map.values(map) # Get all values
# Nested operations (Elixir 1.6.0+)
get_in(map, [:path, :to, :value]) # Safe nested access
put_in(map, [:path, :to, :key], val) # Set nested value
update_in(map, [:path, :to, :key], fun) # Update nested value
Working with Both: Common Patterns
Converting Between Lists and Maps
# List of tuples to map
pairs = [name: "Alice", age: 30]
user = Enum.into(pairs, %{})
# Map to list of tuples
pairs = Map.to_list(user)
# List of maps to map by key
users = [
%{id: 1, name: "Alice"},
%{id: 2, name: "Bob"}
]
users_by_id = Map.new(users, &{&1.id, &1})
Processing Collections
defmodule DataProcessor do
# Group list into map
def group_by_role(users) do
Enum.group_by(users, & &1.role)
end
# Index list by key
def index_by_id(users) do
Map.new(users, &{&1.id, &1})
end
# Transform map values
def normalize_names(users) do
Map.new(users, fn {id, user} ->
{id, %{user | name: String.downcase(user.name)}}
end)
end
# Filter map by predicate
def active_users(users) do
users
|> Enum.filter(fn {_id, user} -> user.active end)
|> Map.new()
end
end
Performance Best Practices
Lists
# ❌ Slow - appending to lists
def slow_build(n) do
Enum.reduce(1..n, [], fn x, acc -> acc ++ [x] end)
end
# ✅ Fast - prepending and reversing
def fast_build(n) do
1..n
|> Enum.reduce([], fn x, acc -> [x | acc] end)
|> Enum.reverse()
end
# ✅ Even better - use Enum.to_list
def best_build(n), do: Enum.to_list(1..n)
Maps
# ❌ Slow - many small updates
def slow_map_build(data) do
Enum.reduce(data, %{}, fn {k, v}, acc ->
Map.put(acc, k, process_value(v))
end)
end
# ✅ Fast - batch operations
def fast_map_build(data) do
data
|> Enum.map(fn {k, v} -> {k, process_value(v)} end)
|> Map.new()
end
Real-World Example: User Management
defmodule UserStore do
# Initial data structure
def new_store do
%{
users: %{}, # Map: user_id => user
emails: %{}, # Map: email => user_id
active_sessions: [] # List: session tokens
}
end
# Add user
def add_user(store, user) do
%{
store |
users: Map.put(store.users, user.id, user),
emails: Map.put(store.emails, user.email, user.id)
}
end
# Find user by email
def find_by_email(store, email) do
with {:ok, user_id} <- Map.fetch(store.emails, email),
{:ok, user} <- Map.fetch(store.users, user_id) do
{:ok, user}
else
:error -> {:error, :not_found}
end
end
# Add session
def add_session(store, token) do
%{store | active_sessions: [token | store.active_sessions]}
end
# Clean expired sessions
def clean_expired_sessions(store, current_time) do
active = Enum.filter(store.active_sessions, &session_valid?(&1, current_time))
%{store | active_sessions: active}
end
defp session_valid?(token, current_time) do
# Implementation details...
end
end
Memory and GC Considerations
- Lists: Share tails between versions (structural sharing)
- Maps: Efficient persistent data structures
- Both: Immutable → old versions can be garbage collected when no longer referenced
When to Use What
Use Lists when:
- Sequential processing
- Building data incrementally (prepend + reverse)
- Pattern matching on structure
- Small collections (< 100 items)
Use Maps when:
- Key-based lookup
- Modeling entities/records
- Frequent updates
- Larger datasets
Understanding these patterns will make your Elixir code both more idiomatic and more performant.
Next up: Mastering recursion patterns with practical examples and performance optimizations.