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.