Maps are like your smartphone's contacts app where each person has a clear name and you can instantly find their information. Instead of scrolling through contact #1, #2, #3 hoping to find the right person, you simply search for "Sofia" or "Marcus" and get exactly what you need. Think of %{name: "Sofia", role: "Engineer", phone: "+1-555-0123"}
as a contact entry where each piece of information has a meaningful label—you know exactly where to find someone's phone number, email, or work details. You can easily add new contacts, update existing information, or organize details however makes sense. In this article, we'll explore how maps work, when they're the perfect choice for your data, and the patterns that make them indispensable for building robust Elixir applications.
Note: The examples in this article use Elixir 1.18.4. While most operations should work across different versions, some functionality might vary.
Table of Contents
- Introduction
- Understanding Maps
- Creating and Updating Maps
- Accessing Map Values
- Pattern Matching with Maps
- Essential Map Functions
- Working with Nested Maps
- Maps vs Other Data Structures
- Real World Applications
- Best Practices
- Conclusion
- Further Reading
- Next Steps
Introduction
Maps are Elixir's key-value data structure, perfect for storing and organizing data where each piece has a meaningful name or identifier. They're one of the most versatile and commonly used data structures in Elixir applications.
What makes maps special:
- Key-value pairs: Each value is associated with a unique key
- Fast lookups: Direct access to values by their keys
- Flexible keys: Keys can be atoms, strings, or any Elixir term
- Dynamic size: Maps can grow and shrink as needed
- Immutable: Like all Elixir data, maps cannot be modified in place
- Pattern matching friendly: Excellent support for destructuring
Think of maps as contact entries where each piece of information has a clear purpose:
- Employee contact:
%{name: "Sofia", role: "developer", department: "engineering"}
- Server details:
%{hostname: "api.server.com", port: 4000, ssl: true}
- API response:
%{"status" => "success", "data" => [...], "message" => "OK"}
Maps excel when you need to:
- Store related information with meaningful labels
- Look up values by name rather than position
- Represent JSON-like data structures
- Build flexible, evolving data schemas
Understanding Maps
The Basics
Maps are collections of key-value pairs enclosed in %{}
syntax:
# Empty map
empty = %{}
# Map with atom keys (most common)
product = %{name: "Gaming Laptop", price: 1299.99, category: "electronics"}
# Map with string keys (common for external data)
api_config = %{"endpoint" => "api.service.com", "timeout" => 5000, "retries" => 3}
# Mixed key types (possible but not recommended)
mixed = %{:status => "active", "count" => 42, 1 => "priority"}
# Maps can contain any data types as values
complex = %{
task_id: 42,
title: "Deploy to production",
tags: [:urgent, :backend],
metadata: %{created_at: ~D[2024-01-15], assigned: true},
transformer: fn x -> String.upcase(x) end
}
Testing in IEx:
iex> employee = %{name: "Sofia", department: "Engineering", level: "Senior"}
%{department: "Engineering", level: "Senior", name: "Sofia"}
iex> api_config = %{"endpoint" => "api.service.com", "timeout" => 5000}
%{"endpoint" => "api.service.com", "timeout" => 5000}
iex> %{}
%{}
Key Types and Conventions
# Atom keys - preferred for internal data
internal = %{user_id: 123, status: :active, type: :premium}
# String keys - common for external data (JSON, APIs)
external = %{"user_id" => 123, "status" => "active", "type" => "premium"}
# Shorthand syntax for atom keys
shorthand = %{title: "DevOps Lead", experience: 5}
# Equivalent to: %{:title => "DevOps Lead", :experience => 5}
# When keys are dynamic variables
key = :dynamic_key
dynamic = %{key => "value"} # Results in %{:dynamic_key => "value"}
computed = %{key => "value", department: "Engineering"}
Testing in IEx:
iex> %{title: "DevOps Lead", experience: 5}
%{experience: 5, title: "DevOps Lead"}
iex> key = :status
:status
iex> %{key => "value", brand: "Toyota"}
%{brand: "Toyota", status: "value"}
Map Properties
product = %{sku: "LAP-001", category: "electronics", price: 1299.99}
# Get map size
size = map_size(product) # 3
# Check if key exists
has_sku = Map.has_key?(product, :sku) # true
has_description = Map.has_key?(product, :description) # false
# Get all keys
keys = Map.keys(product) # [:category, :price, :sku] (order may vary)
# Get all values
values = Map.values(product) # ["electronics", 1299.99, "LAP-001"] (order may vary)
Testing in IEx:
iex> product = %{sku: "LAP-001", category: "electronics", price: 1299.99}
%{category: "electronics", price: 1299.99, sku: "LAP-001"}
iex> map_size(product)
3
iex> Map.has_key?(product, :sku)
true
iex> Map.keys(product)
[:category, :price, :sku]
Creating and Updating Maps
Creating Maps
# Literal syntax
task = %{title: "Deploy API", priority: :urgent}
# From keyword lists
from_keywords = Map.new([brand: "Tesla", model: "Model 3"])
# From lists of tuples
from_tuples = Map.new([{"sensor_id", "TEMP-001"}, {"___location", "Server Room"}])
# With Enum.into
from_enum = ["a", "b", "c"] |> Enum.with_index() |> Enum.into(%{})
# Results in %{"a" => 0, "b" => 1, "c" => 2}
# Using Map.put to build gradually
empty = %{}
step1 = Map.put(empty, :event, "Tech Conference 2024")
step2 = Map.put(step1, :capacity, 500)
# Results in %{capacity: 500, event: "Tech Conference 2024"}
Testing in IEx:
iex> Map.new([brand: "Tesla", model: "Model 3"])
%{brand: "Tesla", model: "Model 3"}
iex> ["a", "b", "c"] |> Enum.with_index() |> Enum.into(%{})
%{"a" => 0, "b" => 1, "c" => 2}
iex> %{} |> Map.put(:event, "Tech Conference 2024") |> Map.put(:capacity, 500)
%{capacity: 500, event: "Tech Conference 2024"}
Updating Maps
Maps are immutable, so "updating" creates new maps:
original = %{sensor_id: "TEMP-001", reading: 23.5, ___location: "Server Room"}
# Update existing key
with_new_reading = %{original | reading: 24.1}
# Results in %{___location: "Server Room", reading: 24.1, sensor_id: "TEMP-001"}
# Add new key (use Map.put for new keys)
with_timestamp = Map.put(original, :timestamp, ~U[2024-01-15 14:30:00Z])
# Update multiple keys
updated = %{original | reading: 24.1, ___location: "Data Center"}
# Conditional update with Map.put_new (only if key doesn't exist)
maybe_added = Map.put_new(original, :unit, "celsius")
already_exists = Map.put_new(original, :sensor_id, "TEMP-002") # Won't change sensor_id
Testing in IEx:
iex> original = %{sensor_id: "TEMP-001", reading: 23.5, ___location: "Server Room"}
%{___location: "Server Room", reading: 23.5, sensor_id: "TEMP-001"}
iex> %{original | reading: 24.1}
%{___location: "Server Room", reading: 24.1, sensor_id: "TEMP-001"}
iex> Map.put(original, :timestamp, ~U[2024-01-15 14:30:00Z])
%{___location: "Server Room", reading: 23.5, sensor_id: "TEMP-001", timestamp: ~U[2024-01-15 14:30:00Z]}
iex> Map.put_new(original, :unit, "celsius")
%{___location: "Server Room", reading: 23.5, sensor_id: "TEMP-001", unit: "celsius"}
iex> original
%{___location: "Server Room", reading: 23.5, sensor_id: "TEMP-001"}
Advanced Update Patterns
defmodule InventoryUpdater do
# Update with function
def increment_stock(product) do
Map.update(product, :stock_count, 0, fn current_stock -> current_stock + 1 end)
end
# Update existing key, or use default if key doesn't exist
def add_view_count(product) do
Map.update(product, :view_count, 1, fn count -> count + 1 end)
end
# Merge maps (right side wins conflicts)
def merge_product_data(product, updates) do
Map.merge(product, updates)
end
# Merge with custom conflict resolution
def smart_merge(product, updates) do
Map.merge(product, updates, fn _key, old_val, new_val ->
if new_val != nil, do: new_val, else: old_val
end)
end
# Delete keys
def remove_internal_data(product) do
Map.drop(product, [:cost_price, :supplier_id])
end
end
Testing in IEx:
iex> product = %{sku: "LAP-001", stock_count: 10}
%{sku: "LAP-001", stock_count: 10}
iex> InventoryUpdater.increment_stock(product)
%{sku: "LAP-001", stock_count: 11}
iex> InventoryUpdater.add_view_count(product)
%{sku: "LAP-001", stock_count: 10, view_count: 1}
iex> updates = %{stock_count: 15, ___location: "Warehouse A"}
%{___location: "Warehouse A", stock_count: 15}
iex> InventoryUpdater.merge_product_data(product, updates)
%{___location: "Warehouse A", sku: "LAP-001", stock_count: 15}
Accessing Map Values
Basic Access Methods
order = %{id: "ORD-2024-001", total: 299.99, status: :shipped}
# Bracket access (returns nil if key doesn't exist)
order_id = order[:id] # "ORD-2024-001"
missing = order[:tracking] # nil
# Map.get with default
total = Map.get(order, :total) # 299.99
tracking = Map.get(order, :tracking, "N/A") # "N/A"
# Dot access (only for atom keys, raises if key doesn't exist)
status_dot = order.status # :shipped
# order.tracking # ** (KeyError) key :tracking not found
# Map.fetch (returns {:ok, value} or :error)
{:ok, total} = Map.fetch(order, :total) # {:ok, 299.99}
:error = Map.fetch(order, :tracking) # :error
# Map.fetch! (raises if key doesn't exist)
status_bang = Map.fetch!(order, :status) # :shipped
# Map.fetch!(order, :tracking) # ** (KeyError)
Testing in IEx:
iex> order = %{id: "ORD-2024-001", total: 299.99, status: :shipped}
%{id: "ORD-2024-001", status: :shipped, total: 299.99}
iex> order[:id]
"ORD-2024-001"
iex> order[:tracking]
nil
iex> Map.get(order, :tracking, "N/A")
"N/A"
iex> order.status
:shipped
iex> Map.fetch(order, :total)
{:ok, 299.99}
iex> Map.fetch(order, :tracking)
:error
Safe Access Patterns
defmodule SafeAccess do
# Safe tracking extraction
def get_tracking(order) do
case Map.fetch(order, :tracking_number) do
{:ok, tracking} -> {:ok, tracking}
:error -> {:error, :tracking_not_found}
end
end
# With default transformation
def get_display_status(order) do
order
|> Map.get(:custom_status)
|> case do
nil -> Map.get(order, :status, :unknown)
custom_status -> custom_status
end
end
# Multiple key access with validation
def get_order_summary(order) do
with {:ok, id} <- Map.fetch(order, :id),
{:ok, total} <- Map.fetch(order, :total) do
{:ok, "Order #{id}: $#{total}"}
else
:error -> {:error, :incomplete_order_info}
end
end
end
Testing in IEx:
iex> order = %{id: "ORD-001", total: 299.99, tracking_number: "TRK-123"}
%{id: "ORD-001", total: 299.99, tracking_number: "TRK-123"}
iex> SafeAccess.get_tracking(order)
{:ok, "TRK-123"}
iex> SafeAccess.get_display_status(order)
:unknown
iex> incomplete = %{id: "ORD-002"}
%{id: "ORD-002"}
iex> SafeAccess.get_order_summary(incomplete)
{:error, :incomplete_order_info}
iex> SafeAccess.get_order_summary(order)
{:ok, "Order ORD-001: $299.99"}
Pattern Matching with Maps
Pattern matching with maps is one of the most powerful features for destructuring and validating data:
Basic Pattern Matching
defmodule MapPatterns do
# Match specific keys
def format_event(%{title: title}) do
"Event: #{title}"
end
# Match multiple keys
def event_summary(%{title: title, capacity: capacity}) do
"#{title} - Max capacity: #{capacity} attendees"
end
# Match with guards
def check_large_event(%{capacity: capacity} = event) when capacity >= 1000 do
{:large_event, event}
end
def check_large_event(event) do
{:small_event, event}
end
# Partial matching (map can have more keys)
def get_event_id(%{id: id}) do
id
end
# Match nested maps
def get_venue_city(%{venue: %{city: city}}) do
city
end
def get_venue_city(_), do: "Unknown"
end
Testing in IEx:
iex> event = %{title: "Tech Conference 2024", capacity: 500, ___location: "Convention Center"}
%{capacity: 500, ___location: "Convention Center", title: "Tech Conference 2024"}
iex> MapPatterns.format_event(event)
"Event: Tech Conference 2024"
iex> MapPatterns.event_summary(event)
"Tech Conference 2024 - Max capacity: 500 attendees"
iex> MapPatterns.check_large_event(event)
{:small_event, %{capacity: 500, ___location: "Convention Center", title: "Tech Conference 2024"}}
iex> event_with_id = %{id: 789, title: "DevOps Summit", category: :technical}
%{category: :technical, id: 789, title: "DevOps Summit"}
iex> MapPatterns.get_event_id(event_with_id)
789
Advanced Pattern Matching
defmodule AdvancedPatterns do
# Match and extract with variable capture
def process_task(%{title: title, tags: tags} = task) do
if :urgent in tags do
{:priority_task, title, task}
else
{:regular_task, title, task}
end
end
# Pattern match in function heads for different cases
def handle_api_response(%{"status" => "success", "data" => data}) do
{:ok, data}
end
def handle_api_response(%{"status" => "error", "message" => message}) do
{:error, message}
end
def handle_api_response(response) do
{:unknown, response}
end
# Match with pin operator (match against existing variable)
def find_task_by_priority(tasks, target_priority) do
Enum.find(tasks, fn %{priority: ^target_priority} -> true; _ -> false end)
end
# Complex nested pattern matching
def extract_sensor_coordinates(%{device: %{___location: {lat, lon}}}) do
{:ok, lat, lon}
end
def extract_sensor_coordinates(_), do: {:error, :no_coordinates}
end
Testing in IEx:
iex> urgent_task = %{title: "Fix production bug", tags: [:urgent, :backend]}
%{tags: [:urgent, :backend], title: "Fix production bug"}
iex> AdvancedPatterns.process_task(urgent_task)
{:priority_task, "Fix production bug", %{tags: [:urgent, :backend], title: "Fix production bug"}}
iex> api_success = %{"status" => "success", "data" => ["sensor1", "sensor2"]}
%{"data" => ["sensor1", "sensor2"], "status" => "success"}
iex> AdvancedPatterns.handle_api_response(api_success)
{:ok, ["sensor1", "sensor2"]}
iex> tasks = [%{title: "Deploy API", priority: :high}, %{title: "Update docs", priority: :low}]
[%{priority: :high, title: "Deploy API"}, %{priority: :low, title: "Update docs"}]
iex> AdvancedPatterns.find_task_by_priority(tasks, :high)
%{priority: :high, title: "Deploy API"}
Pattern Matching in case Expressions
defmodule ResponseProcessor do
def process_api_response(response) do
case response do
%{status: 200, body: %{"data" => data}} ->
{:success, data}
%{status: 404} ->
{:error, :not_found}
%{status: status} when status >= 500 ->
{:error, :server_error}
%{status: status, body: %{"error" => message}} ->
{:error, "HTTP #{status}: #{message}"}
%{error: reason} ->
{:error, reason}
_ ->
{:error, :unknown_response}
end
end
def categorize_user(user) do
case user do
%{age: age, role: :admin} when age >= 21 ->
:senior_admin
%{age: age, role: :admin} when age < 21 ->
:junior_admin
%{age: age, subscription: :premium} when age >= 18 ->
:premium_adult
%{age: age} when age >= 18 ->
:regular_adult
%{age: age} when age < 18 ->
:minor
_ ->
:unknown
end
end
end
Testing in IEx:
iex> good_response = %{status: 200, body: %{"data" => ["item1", "item2"]}}
%{body: %{"data" => ["item1", "item2"]}, status: 200}
iex> ResponseProcessor.process_api_response(good_response)
{:success, ["item1", "item2"]}
iex> not_found = %{status: 404, body: %{"error" => "Not found"}}
%{body: %{"error" => "Not found"}, status: 404}
iex> ResponseProcessor.process_api_response(not_found)
{:error, :not_found}
iex> junior_admin = %{name: "Sofia", age: 19, role: :admin}
%{age: 19, name: "Sofia", role: :admin}
iex> ResponseProcessor.categorize_user(junior_admin)
:junior_admin
iex> regular_dev = %{name: "Marcus", age: 25, role: :developer}
%{age: 25, name: "Marcus", role: :developer}
iex> ResponseProcessor.categorize_user(regular_dev)
:regular_adult
Essential Map Functions
Basic Map Operations
base_config = %{host: "localhost", port: 4000, ssl: true}
# Adding and updating
updated = Map.put(base_config, :timeout, 5000)
port_updated = Map.put(base_config, :port, 8080)
# Conditional operations
maybe_added = Map.put_new(base_config, :retries, 3)
wont_change = Map.put_new(base_config, :host, "remote.server.com") # host already exists
# Removing keys
without_ssl = Map.delete(base_config, :ssl)
clean_config = Map.drop(base_config, [:ssl, :port])
# Taking specific keys
subset = Map.take(base_config, [:host, :port])
Testing in IEx:
iex> base_config = %{host: "localhost", port: 4000, ssl: true}
%{host: "localhost", port: 4000, ssl: true}
iex> Map.put(base_config, :timeout, 5000)
%{host: "localhost", port: 4000, ssl: true, timeout: 5000}
iex> Map.delete(base_config, :ssl)
%{host: "localhost", port: 4000}
iex> Map.take(base_config, [:host, :port])
%{host: "localhost", port: 4000}
Transformation Functions
defmodule MapTransformations do
# Transform all values
def increment_numbers(map) do
Map.new(map, fn {key, value} ->
new_value = if is_number(value), do: value + 1, else: value
{key, new_value}
end)
end
# Filter by condition
def filter_adults(users) do
users
|> Enum.filter(fn {_id, %{age: age}} -> age >= 18 end)
|> Map.new()
end
# Transform keys
def stringify_keys(map) do
Map.new(map, fn {key, value} -> {to_string(key), value} end)
end
def atomize_keys(map) do
Map.new(map, fn {key, value} -> {String.to_atom(key), value} end)
end
# Combine multiple maps
def merge_user_data(base_data, profile_data, preferences) do
base_data
|> Map.merge(profile_data)
|> Map.merge(preferences)
end
end
Testing in IEx:
iex> test_map = %{temperature: 23, status: "active", score: 85}
%{score: 85, status: "active", temperature: 23}
iex> MapTransformations.increment_numbers(test_map)
%{score: 86, status: "active", temperature: 24}
iex> string_map = %{"sensor_type" => "temperature", "reading" => "23.5"}
%{"reading" => "23.5", "sensor_type" => "temperature"}
iex> MapTransformations.atomize_keys(string_map)
%{reading: "23.5", sensor_type: "temperature"}
iex> MapTransformations.stringify_keys(%{sensor_type: :temperature, reading: 23.5})
%{"reading" => 23.5, "sensor_type" => "temperature"}
Working with Collections of Maps
defmodule MapCollections do
# Process list of product maps
def get_names(products) do
Enum.map(products, &Map.get(&1, :name))
end
# Filter and transform
def get_expensive_skus(products) do
products
|> Enum.filter(fn %{price: price} -> price >= 1000 end)
|> Enum.map(fn %{sku: sku} -> sku end)
end
# Group by attribute
def group_by_price_range(products) do
products
|> Enum.group_by(fn %{price: price} ->
cond do
price < 100 -> :budget
price < 1000 -> :mid_range
true -> :premium
end
end)
end
# Calculate statistics
def calculate_average_price(products) do
prices = Enum.map(products, &Map.get(&1, :price))
Enum.sum(prices) / length(prices)
end
# Find by criteria
def find_premium_product(products) do
Enum.find(products, fn product ->
Map.get(product, :category) == :premium
end)
end
end
Testing in IEx:
iex> products = [%{name: "Laptop", price: 1299.99, sku: "LAP-001", category: :electronics}, %{name: "Mouse", price: 29.99, sku: "MOU-001", category: :accessories}, %{name: "Monitor", price: 899.99, sku: "MON-001", category: :premium}]
[%{category: :electronics, name: "Laptop", price: 1299.99, sku: "LAP-001"}, %{category: :accessories, name: "Mouse", price: 29.99, sku: "MOU-001"}, %{category: :premium, name: "Monitor", price: 899.99, sku: "MON-001"}]
iex> MapCollections.get_names(products)
["Laptop", "Mouse", "Monitor"]
iex> MapCollections.get_expensive_skus(products)
["LAP-001"]
iex> MapCollections.find_premium_product(products)
%{category: :premium, name: "Monitor", price: 899.99, sku: "MON-001"}
Working with Nested Maps
Nested maps are common when dealing with complex data structures, APIs, and configurations:
Creating Nested Maps
# Server configuration with nested monitoring and security
server_config = %{
id: "SRV-001",
hostname: "api.service.com",
region: "us-east-1",
networking: %{
port: 443,
protocol: "https",
load_balancer: "ALB-001",
coordinates: %{datacenter: "DC-1", rack: "R-15"}
},
monitoring: %{
alerts: "enabled",
metrics: %{
cpu: true,
memory: true,
disk: false
},
retention: "30d"
}
}
# API response with nested data
api_response = %{
status: "success",
data: %{
sensors: [
%{id: "TEMP-001", type: "temperature"},
%{id: "HUMID-002", type: "humidity"}
],
metadata: %{
total: 2,
page: 1,
per_page: 10
}
}
}
Accessing Nested Values
defmodule NestedAccess do
# Safe nested access
def get_user_city(user) do
user
|> Map.get(:address, %{})
|> Map.get(:city)
end
# Deep access with get_in
def get_notification_preference(user, type) do
get_in(user, [:preferences, :notifications, type])
end
# Pattern matching for nested extraction
def extract_coordinates(%{address: %{coordinates: coords}}) do
{:ok, coords}
end
def extract_coordinates(_), do: {:error, :no_coordinates}
# Multiple level extraction
def get_contact_info(user) do
with {:ok, name} <- Map.fetch(user, :name),
{:ok, email} <- Map.fetch(user, :email),
city <- get_user_city(user) do
%{name: name, email: email, city: city}
end
end
end
Testing in IEx:
iex> user_profile = %{name: "Sofia", address: %{city: "Barcelona", state: "Catalonia"}, preferences: %{notifications: %{email: true, sms: false}}}
%{address: %{city: "Barcelona", state: "Catalonia"}, name: "Sofia", preferences: %{notifications: %{email: true, sms: false}}}
iex> NestedAccess.get_user_city(user_profile)
"Barcelona"
iex> NestedAccess.get_notification_preference(user_profile, :email)
true
iex> get_in(user_profile, [:preferences, :notifications, :sms])
false
iex> get_in(user_profile, [:preferences, :notifications, :push])
nil
Updating Nested Maps
defmodule NestedUpdates do
# Update nested value with put_in
def update_city(user, new_city) do
put_in(user, [:address, :city], new_city)
end
# Update with function using update_in
def increment_login_count(user) do
update_in(user, [:stats, :login_count], fn
nil -> 1
count -> count + 1
end)
end
# Safe nested update (create path if doesn't exist)
def enable_notification(user, type) do
user
|> put_in([:preferences], Map.get(user, :preferences, %{}))
|> put_in([:preferences, :notifications],
get_in(user, [:preferences, :notifications]) || %{})
|> put_in([:preferences, :notifications, type], true)
end
# Complex nested transformation
def normalize_address(user) do
update_in(user, [:address], fn address ->
address
|> Map.update(:city, "", &String.upcase/1)
|> Map.update(:state, "", &String.upcase/1)
end)
end
end
Testing in IEx:
iex> user_data = %{name: "Marcus", address: %{city: "madrid", state: "madrid"}}
%{address: %{city: "madrid", state: "madrid"}, name: "Marcus"}
iex> NestedUpdates.update_city(user_data, "Barcelona")
%{address: %{city: "Barcelona", state: "madrid"}, name: "Marcus"}
iex> NestedUpdates.normalize_address(user_data)
%{address: %{city: "MADRID", state: "MADRID"}, name: "Marcus"}
iex> simple_user = %{name: "Elena"}
%{name: "Elena"}
iex> NestedUpdates.enable_notification(simple_user, :email)
%{name: "Elena", preferences: %{notifications: %{email: true}}}
Complex Nested Operations
defmodule ComplexNested do
# Merge nested maps intelligently
def deep_merge(map1, map2) do
Map.merge(map1, map2, fn _key, val1, val2 ->
if is_map(val1) and is_map(val2) do
deep_merge(val1, val2)
else
val2
end
end)
end
# Flatten nested structure
def flatten_user(user) do
base = Map.take(user, [:id, :name, :email])
address = Map.get(user, :address, %{})
base
|> Map.put(:city, Map.get(address, :city))
|> Map.put(:state, Map.get(address, :state))
|> Map.put(:zip, Map.get(address, :zip))
end
# Transform nested collections
def transform_nested_users(response) do
update_in(response, [:data, :users], fn users ->
Enum.map(users, fn user ->
Map.update(user, :name, "", &String.upcase/1)
end)
end)
end
end
Testing in IEx:
iex> map1 = %{a: 1, b: %{c: 2, d: 3}}
%{a: 1, b: %{c: 2, d: 3}}
iex> map2 = %{a: 10, b: %{d: 30, e: 40}}
%{a: 10, b: %{d: 30, e: 40}}
iex> ComplexNested.deep_merge(map1, map2)
%{a: 10, b: %{c: 2, d: 30, e: 40}}
iex> user_with_address = %{id: "USR-001", name: "Viktor", email: "[email protected]", address: %{city: "Stockholm", state: "Stockholm", zip: "11122"}}
%{address: %{city: "Stockholm", state: "Stockholm", zip: "11122"}, email: "[email protected]", id: "USR-001", name: "Viktor"}
iex> ComplexNested.flatten_user(user_with_address)
%{city: "Stockholm", email: "[email protected]", id: "USR-001", name: "Viktor", state: "Stockholm", zip: "11122"}
Maps vs Other Data Structures
Understanding when to choose maps over other data structures is crucial for writing idiomatic Elixir:
Maps vs Keyword Lists
# Use keyword lists for:
options = [host: "localhost", port: 4000, ssl: true] # Function options
config = [debug: true, log_level: :info] # Small configs
ecto_query = from(u in User, where: u.age > 18) # DSL parameters
# Use maps for:
employee_data = %{id: 1001, role: "developer", department: "engineering"} # Structured data
cache = %{"session:1" => %{status: "active"}, "session:2" => %{status: "expired"}} # Fast lookups
response = %{status: :ok, data: [...], message: "Success"} # Complex data
# Conversion between them
keyword_to_map = Enum.into(options, %{}) # %{host: "localhost", port: 4000, ssl: true}
map_to_keyword = Enum.into(user_data, []) # Not recommended - loses map benefits
Maps vs Tuples
# Use tuples for:
coordinate = {10, 20} # Fixed position data
result = {:ok, "success"} # Tagged return values
version = {1, 2, 3} # Fixed number of elements
# Use maps for:
point = %{x: 10, y: 20} # Named fields
api_result = %{status: :ok, data: "success", meta: %{}} # Flexible structure
task = %{id: 42, title: "Deploy API", priority: :urgent} # Record-like data
# When you need to add/remove fields, maps are better:
extended_point = Map.put(point, :z, 30) # Easy to extend
# Can't easily extend a tuple: {10, 20} → {10, 20, 30} requires full reconstruction
Maps vs Structs
# Maps: Flexible, dynamic structure
product_map = %{sku: "LAP-001", price: 1299.99, category: "electronics"}
extended_product = Map.put(product_map, :warranty, "2 years") # Easy to extend
# Structs: Fixed structure, type safety
defmodule Product do
defstruct [:sku, :price, :category]
end
product_struct = %Product{sku: "LAP-001", price: 1299.99, category: "electronics"}
# product_struct.warranty # This would raise an error - warranty not defined
# Choose maps when:
# - Working with external data (JSON, APIs)
# - Structure might change
# - Need flexible key-value storage
# Choose structs when:
# - Modeling ___domain entities
# - Want compile-time guarantees
# - Need pattern matching on type
Testing in IEx:
iex> options = [host: "localhost", port: 4000]
[host: "localhost", port: 4000]
iex> Enum.into(options, %{})
%{host: "localhost", port: 4000}
iex> point_tuple = {10, 20}
{10, 20}
iex> point_map = %{x: 10, y: 20}
%{x: 10, y: 20}
iex> Map.put(point_map, :z, 30)
%{x: 10, y: 20, z: 30}
Real World Applications
API Response Handling
defmodule APIHandler do
# Process weather API response
def process_weather_data(response) do
case response do
%{"temperature" => temp, "humidity" => humidity, "city" => city} ->
{:ok, %{
temperature: temp,
humidity: humidity,
___location: city,
wind_speed: Map.get(response, "wind_speed", 0),
pressure: Map.get(response, "pressure", 0)
}}
%{"error" => "City not found"} ->
{:error, :location_not_found}
_ ->
{:error, :invalid_response}
end
end
# Handle paginated API responses
def extract_paginated_data(%{"data" => items, "pagination" => pagination}) do
%{
items: items,
current_page: Map.get(pagination, "page", 1),
total_pages: Map.get(pagination, "pages", 1),
total_items: Map.get(pagination, "total", 0),
has_next: Map.get(pagination, "has_next", false)
}
end
end
Configuration Management
defmodule AppConfig do
# Application configuration with defaults
def load_config(env \\ :dev) do
base_config = %{
app_name: "MyApp",
port: 4000,
database: %{
adapter: "postgres",
pool_size: 10
},
features: %{
analytics: false,
caching: true
}
}
env_config = load_env_config(env)
deep_merge_config(base_config, env_config)
end
# Environment-specific overrides
defp load_env_config(:prod) do
%{
port: 80,
database: %{
pool_size: 20,
timeout: 30_000
},
features: %{
analytics: true,
caching: true
}
}
end
defp load_env_config(:test) do
%{
database: %{
adapter: "sqlite",
database: ":memory:"
},
features: %{
analytics: false,
caching: false
}
}
end
defp load_env_config(_), do: %{}
defp deep_merge_config(base, override) do
Map.merge(base, override, fn _key, base_val, override_val ->
if is_map(base_val) and is_map(override_val) do
deep_merge_config(base_val, override_val)
else
override_val
end
end)
end
end
Data Transformation Pipeline
defmodule DataPipeline do
# Transform raw CSV data into structured format
def process_user_csv(csv_rows) do
csv_rows
|> Enum.map(&parse_csv_row/1)
|> Enum.filter(&valid_user?/1)
|> Enum.map(&normalize_user/1)
|> Enum.group_by(& &1.department)
end
defp parse_csv_row([name, email, age, department, salary]) do
%{
name: String.trim(name),
email: String.downcase(String.trim(email)),
age: String.to_integer(age),
department: String.trim(department),
salary: String.to_float(salary)
}
end
defp valid_user?(%{email: email, age: age}) do
String.contains?(email, "@") and age >= 18
end
defp normalize_user(%{name: name, department: dept} = user) do
%{user |
name: String.split(name) |> Enum.map(&String.capitalize/1) |> Enum.join(" "),
department: String.downcase(dept)
}
end
# Build user analytics
def calculate_department_stats(users_by_dept) do
Map.new(users_by_dept, fn {dept, users} ->
stats = %{
count: length(users),
avg_age: average_age(users),
avg_salary: average_salary(users),
age_range: age_range(users)
}
{dept, stats}
end)
end
defp average_age(users) do
(users |> Enum.map(& &1.age) |> Enum.sum()) / length(users)
end
defp average_salary(users) do
(users |> Enum.map(& &1.salary) |> Enum.sum()) / length(users)
end
defp age_range(users) do
ages = Enum.map(users, & &1.age)
%{min: Enum.min(ages), max: Enum.max(ages)}
end
end
Testing in IEx:
iex> weather_response = %{"temperature" => 23.5, "humidity" => 65, "city" => "Barcelona", "wind_speed" => 12.3}
%{"city" => "Barcelona", "humidity" => 65, "temperature" => 23.5, "wind_speed" => 12.3}
iex> APIHandler.process_weather_data(weather_response)
{:ok, %{humidity: 65, ___location: "Barcelona", pressure: 0, temperature: 23.5, wind_speed: 12.3}}
iex> AppConfig.load_config(:prod)
%{app_name: "MyApp", database: %{adapter: "postgres", pool_size: 20, timeout: 30000}, features: %{analytics: true, caching: true}, port: 80}
Best Practices
Do's and Don'ts
✅ DO: Use atom keys for internal data
# Good - fast access, clear intent
employee = %{name: "Sofia", experience: 5, role: :lead}
✅ DO: Use string keys for external data
# Good - matches JSON APIs, external sources
api_data = %{"user_id" => 123, "status" => "active"}
✅ DO: Pattern match to extract needed data
# Good - clear and safe
def get_user_email(%{email: email}), do: {:ok, email}
def get_user_email(_), do: {:error, :no_email}
✅ DO: Use Map.get with defaults for optional values
# Good - safe with sensible defaults
display_name = Map.get(user, :display_name, user.name)
❌ DON'T: Mix atom and string keys unnecessarily
# Confusing - pick one style
mixed = %{status: :active, "priority" => "high"}
❌ DON'T: Use maps for ordered data
# Bad - maps don't guarantee order
sequence = %{1 => "first", 2 => "second", 3 => "third"}
# Good - use lists for ordered data
sequence = ["first", "second", "third"]
Performance Considerations
defmodule PerformanceTips do
# Fast: Direct key access
def get_name_fast(%{name: name}), do: name
# Slower: Function call
def get_name_slow(user), do: Map.get(user, :name)
# Fast: Update existing key
def update_age_fast(user, new_age), do: %{user | age: new_age}
# Slower: Map.put (but works for new keys too)
def update_age_slower(user, new_age), do: Map.put(user, :age, new_age)
# Good: Batch updates
def update_user_info(user, updates) do
Map.merge(user, updates)
end
# Less efficient: Individual updates
def update_user_info_slow(user, name, age, email) do
user
|> Map.put(:name, name)
|> Map.put(:age, age)
|> Map.put(:email, email)
end
end
Error Handling Patterns
defmodule SafeMapOperations do
# Safe extraction with defaults
def safe_get(map, key, default \\ nil) do
Map.get(map, key, default)
end
# Validation before processing
def process_user(user_data) do
with {:ok, user} <- validate_required_fields(user_data),
{:ok, normalized} <- normalize_user_data(user) do
{:ok, normalized}
end
end
defp validate_required_fields(data) do
required = [:name, :email, :age]
case Enum.find(required, &(!Map.has_key?(data, &1))) do
nil -> {:ok, data}
missing_key -> {:error, {:missing_field, missing_key}}
end
end
defp normalize_user_data(user) do
try do
normalized = %{
name: String.trim(user.name),
email: String.downcase(user.email),
age: ensure_integer(user.age)
}
{:ok, normalized}
rescue
_ -> {:error, :normalization_failed}
end
end
defp ensure_integer(value) when is_integer(value), do: value
defp ensure_integer(value) when is_binary(value), do: String.to_integer(value)
defp ensure_integer(_), do: raise("Invalid integer value")
end
Testing in IEx:
iex> good_data = %{name: " Elena Martinez ", email: "[email protected]", age: "28"}
%{age: "28", email: "[email protected]", name: " Elena Martinez "}
iex> SafeMapOperations.process_user(good_data)
{:ok, %{age: 28, email: "[email protected]", name: "Elena Martinez"}}
iex> bad_data = %{name: "Viktor", email: "[email protected]"}
%{email: "[email protected]", name: "Viktor"}
iex> SafeMapOperations.process_user(bad_data)
{:error, {:missing_field, :age}}
Conclusion
Maps are one of the most versatile and powerful data structures in Elixir, providing the foundation for modeling complex data in functional applications. In this article, we've explored:
- How maps work internally with key-value pairs
- Creating and updating maps with various techniques
- Accessing values safely with different approaches
- Pattern matching for powerful data extraction
- Essential map functions for transformation and manipulation
- Working with nested maps for complex data structures
- Choosing between maps and other data structures
- Real-world applications in APIs, configuration, and data processing
- Best practices for performance and error handling
Key takeaways:
- Maps excel at key-value storage with fast lookups
- Use atom keys for internal data, string keys for external data
- Pattern matching makes map code elegant and safe
- Nested maps handle complex hierarchical data well
- Choose maps for flexible, dynamic data structures
- Combine maps with other data structures for optimal solutions
- Always consider immutability when updating maps
Maps form the backbone of many Elixir applications, from web servers handling JSON to configuration systems managing complex settings. Master maps, and you'll have the tools to model almost any kind of data your applications need.
Further Reading
- Elixir Official Documentation - Map
- Elixir School - Maps
- Programming Elixir by Dave Thomas - Maps Chapter
Next Steps
With a solid understanding of maps, you're ready to explore Keyword Lists in Elixir. Keyword lists provide an ordered collection of key-value pairs that's perfect for function options, small configurations, and DSL parameters.
In the next article, we'll explore:
- Creating and manipulating keyword lists
- When to choose keyword lists over maps
- Pattern matching with keyword lists
- Function options and configuration patterns
- Working with duplicate keys
- Performance characteristics and trade-offs
Keyword lists complete our journey through Elixir's core data structures, giving you all the tools needed to choose the right structure for any situation!
Top comments (0)