Collections
Master Elixir's collection types and the powerful Enum and Stream modules for data transformation.
Lists
Linked lists - fast prepend, slow append and random access.
Creating Lists
[1, 2, 3, 4, 5]
["apple", "banana", "cherry"]
[1, "mixed", :types, true, %{key: "value"}]
[] # Empty list
List Operations
# Prepend (fast - O(1))
[0 | [1, 2, 3]] # => [0, 1, 2, 3]
# Append (slow - O(n))
[1, 2, 3] ++ [4] # => [1, 2, 3, 4]
# Concatenation
[1, 2] ++ [3, 4] ++ [5, 6] # => [1, 2, 3, 4, 5, 6]
# Subtraction
[1, 2, 3, 4] -- [2, 4] # => [1, 3]
[1, 2, 1, 3] -- [1] # => [2, 1, 3] (removes first occurrence)
# Membership
3 in [1, 2, 3] # => true
# Head and tail
[head | tail] = [1, 2, 3, 4]
# head = 1, tail = [2, 3, 4]
hd([1, 2, 3]) # => 1
tl([1, 2, 3]) # => [2, 3]
List Module
# Length
length([1, 2, 3]) # => 3
# First/last
List.first([1, 2, 3]) # => 1
List.last([1, 2, 3]) # => 3
# Insert
List.insert_at([1, 2, 4], 2, 3) # => [1, 2, 3, 4]
# Delete
List.delete([1, 2, 3, 2], 2) # => [1, 3, 2] (first occurrence)
# Flatten
List.flatten([[1, 2], [3, 4]]) # => [1, 2, 3, 4]
List.flatten([1, [2, [3, 4]]]) # => [1, 2, 3, 4]
# Zip
List.zip([[1, 2], [3, 4], [5, 6]]) # => [{1, 3, 5}, {2, 4, 6}]
# Duplicate
List.duplicate("x", 3) # => ["x", "x", "x"]
Tuples
Fixed-size, contiguous memory - fast access, not for dynamic growth.
Creating Tuples
{1, 2, 3}
{:ok, "result"}
{"Alice", 30, :engineer}
Tuple Operations
# Size (fast - O(1))
tuple_size({1, 2, 3}) # => 3
# Access element
elem({:ok, 42}, 1) # => 42
# Update element (creates new tuple)
tuple = {:ok, 42, "message"}
put_elem(tuple, 1, 100) # => {:ok, 100, "message"}
# Pattern matching
{:ok, value} = {:ok, 42}
# value = 42
Common Tuple Patterns
# Status tuples
{:ok, result}
{:error, reason}
# Multiple return values
{status, value, metadata}
# Coordinates
{x, y}
{x, y, z}
Keyword Lists
Ordered list of {atom, value} tuples - used for options.
Creating Keyword Lists
# These are equivalent
[{:name, "Alice"}, {:age, 30}]
[name: "Alice", age: 30]
# Duplicate keys allowed
[name: "Alice", name: "Bob"]
# Mixed with other atoms
[name: "Alice", :atom, age: 30] # Last one isn't a keyword pair
Accessing Keyword Lists
list = [name: "Alice", age: 30]
# Access
list[:name] # => "Alice"
Keyword.get(list, :age) # => 30
Keyword.get(list, :missing) # => nil
Keyword.get(list, :missing, "default") # => "default"
# Get all values for key (with duplicates)
list = [name: "Alice", name: "Bob", age: 30]
Keyword.get_values(list, :name) # => ["Alice", "Bob"]
Keyword Module
# Put (adds to front)
Keyword.put([a: 1], :b, 2) # => [b: 2, a: 1]
# Delete
Keyword.delete([a: 1, b: 2], :a) # => [b: 2]
# Merge
Keyword.merge([a: 1, b: 2], [b: 3, c: 4])
# => [a: 1, b: 3, c: 4]
# Keys
Keyword.keys([a: 1, b: 2]) # => [:a, :b]
# Values
Keyword.values([a: 1, b: 2]) # => [1, 2]
# Has key?
Keyword.has_key?([a: 1], :a) # => true
Function Options Pattern
defmodule FileReader do
def read(path, opts \\ []) do
encoding = Keyword.get(opts, :encoding, "utf-8")
trim = Keyword.get(opts, :trim, false)
# Read file with options...
end
end
FileReader.read("file.txt")
FileReader.read("file.txt", encoding: "ascii", trim: true)
Maps
Key-value stores - efficient lookup, update, and pattern matching.
Creating Maps
# Any type as key
%{"name" => "Alice", "age" => 30}
%{name: "Alice", age: 30} # Atom keys (most common)
%{1 => "one", 2 => "two"}
# Empty map
%{}
# String keys vs atom keys
string_map = %{"name" => "Alice"}
atom_map = %{name: "Alice"}
Accessing Maps
user = %{name: "Alice", age: 30}
# Bracket notation (works for any key type)
user[:name] # => "Alice"
user[:missing] # => nil
# Dot notation (only for atom keys)
user.name # => "Alice"
user.missing # ** (KeyError)
# Fetch (returns tuple)
Map.fetch(user, :name) # => {:ok, "Alice"}
Map.fetch(user, :missing) # => :error
# Get with default
Map.get(user, :email, "no-email") # => "no-email"
Updating Maps
user = %{name: "Alice", age: 30}
# Update existing key
%{user | age: 31} # => %{name: "Alice", age: 31}
# Update syntax only works with existing keys
%{user | email: "alice@example.com"} # ** (KeyError)
# Put (works for new or existing)
Map.put(user, :email, "alice@example.com")
# => %{name: "Alice", age: 30, email: "alice@example.com"}
# Put new (only if key doesn't exist)
Map.put_new(user, :name, "Bob") # => %{name: "Alice", age: 30}
Map.put_new(user, :city, "NYC") # => %{name: "Alice", age: 30, city: "NYC"}
# Delete
Map.delete(user, :age) # => %{name: "Alice"}
# Merge
Map.merge(user, %{city: "NYC", age: 31})
# => %{name: "Alice", age: 31, city: "NYC"}
Map Module
# Keys
Map.keys(%{a: 1, b: 2}) # => [:a, :b]
# Values
Map.values(%{a: 1, b: 2}) # => [1, 2]
# To list
Map.to_list(%{a: 1, b: 2}) # => [a: 1, b: 2]
# Has key?
Map.has_key?(%{a: 1}, :a) # => true
# Drop keys
Map.drop(%{a: 1, b: 2, c: 3}, [:b, :c]) # => %{a: 1}
# Take keys
Map.take(%{a: 1, b: 2, c: 3}, [:a, :c]) # => %{a: 1, c: 3}
# Update with function
Map.update(%{a: 1}, :a, 0, &(&1 + 10)) # => %{a: 11}
Nested Maps
user = %{
name: "Alice",
address: %{
street: "123 Main St",
city: "NYC"
}
}
# Access
user.address.city # => "NYC"
# Update nested (creates new map)
put_in(user.address.city, "Boston")
# => %{name: "Alice", address: %{street: "123 Main St", city: "Boston"}}
# Update with function
update_in(user.address.city, &String.upcase/1)
# => %{name: "Alice", address: %{street: "123 Main St", city: "NYC"}}
# Get nested
get_in(user, [:address, :city]) # => "NYC"
MapSet
Unordered collection of unique values.
# Create
set1 = MapSet.new([1, 2, 3, 2, 1]) # => #MapSet<[1, 2, 3]>
# Put
MapSet.put(set1, 4) # => #MapSet<[1, 2, 3, 4]>
# Delete
MapSet.delete(set1, 2) # => #MapSet<[1, 3]>
# Member?
MapSet.member?(set1, 2) # => true
# Size
MapSet.size(set1) # => 3
# Union
set2 = MapSet.new([3, 4, 5])
MapSet.union(set1, set2) # => #MapSet<[1, 2, 3, 4, 5]>
# Intersection
MapSet.intersection(set1, set2) # => #MapSet<[3]>
# Difference
MapSet.difference(set1, set2) # => #MapSet<[1, 2]>
# Subset?
MapSet.subset?(MapSet.new([1, 2]), set1) # => true
Range
Sequence of integers.
# Create
1..10
-5..5
# Membership
5 in 1..10 # => true
# To list
Enum.to_list(1..5) # => [1, 2, 3, 4, 5]
# In comprehension
for n <- 1..5, do: n * 2 # => [2, 4, 6, 8, 10]
# Reverse range
10..1//-1 # Step by -1
Enum Module
Work with any enumerable (lists, maps, ranges, etc.).
Transformation
# Map - transform each element
Enum.map([1, 2, 3], fn x -> x * 2 end)
# => [2, 4, 6]
Enum.map([1, 2, 3], &(&1 * 2)) # Shorthand
# => [2, 4, 6]
# Filter - select elements
Enum.filter([1, 2, 3, 4], fn x -> rem(x, 2) == 0 end)
# => [2, 4]
# Reject - opposite of filter
Enum.reject([1, 2, 3, 4], &(rem(&1, 2) == 0))
# => [1, 3]
# Reduce - accumulate
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> x + acc end)
# => 10
Enum.reduce([1, 2, 3, 4], &+/2) # Shorthand
# => 10
Queries
# all? - Check if all match
Enum.all?([2, 4, 6], &(rem(&1, 2) == 0)) # => true
# any? - Check if any match
Enum.any?([1, 2, 3], &(&1 > 2)) # => true
# member? - Check membership
Enum.member?([1, 2, 3], 2) # => true
# find - Get first match
Enum.find([1, 2, 3, 4], &(&1 > 2)) # => 3
# count - Count elements
Enum.count([1, 2, 3]) # => 3
Enum.count([1, 2, 3, 4], &(rem(&1, 2) == 0)) # => 2 (count matching)
Access
# at - Get element at index
Enum.at([1, 2, 3], 1) # => 2
Enum.at([1, 2, 3], 10) # => nil
Enum.at([1, 2, 3], 10, :default) # => :default
# fetch - Get element (returns tuple)
Enum.fetch([1, 2, 3], 1) # => {:ok, 2}
Enum.fetch([1, 2, 3], 10) # => :error
# Random
Enum.random([1, 2, 3, 4, 5]) # => random element
# Take
Enum.take([1, 2, 3, 4, 5], 3) # => [1, 2, 3]
Enum.take([1, 2, 3, 4, 5], -2) # => [4, 5] (from end)
# Drop
Enum.drop([1, 2, 3, 4, 5], 2) # => [3, 4, 5]
# Slice
Enum.slice([1, 2, 3, 4, 5], 1..3) # => [2, 3, 4]
Aggregation
# Sum
Enum.sum([1, 2, 3, 4]) # => 10
# Max/Min
Enum.max([3, 1, 4, 2]) # => 4
Enum.min([3, 1, 4, 2]) # => 1
# Min/Max by function
users = [%{name: "Alice", age: 30}, %{name: "Bob", age: 25}]
Enum.max_by(users, fn user -> user.age end)
# => %{name: "Alice", age: 30}
# Join (lists only)
Enum.join(["a", "b", "c"], "-") # => "a-b-c"
Sorting
# Sort
Enum.sort([3, 1, 4, 2]) # => [1, 2, 3, 4]
# Sort descending
Enum.sort([3, 1, 4, 2], :desc) # => [4, 3, 2, 1]
# Sort by function
users = [%{name: "Charlie", age: 35}, %{name: "Alice", age: 30}]
Enum.sort_by(users, fn user -> user.age end)
# => [%{name: "Alice", age: 30}, %{name: "Charlie", age: 35}]
# Sort with custom comparator
Enum.sort([3, 1, 4, 2], &(&1 > &2)) # => [4, 3, 2, 1]
Grouping
# Group by
words = ["ant", "buffalo", "cat", "dingo"]
Enum.group_by(words, &String.length/1)
# => %{3 => ["ant", "cat"], 5 => ["dingo"], 7 => ["buffalo"]}
# Chunk every
Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
# => [[1, 2], [3, 4], [5, 6]]
# Chunk by
Enum.chunk_by([1, 1, 2, 2, 2, 3, 3], &(&1))
# => [[1, 1], [2, 2, 2], [3, 3]]
# Split
Enum.split([1, 2, 3, 4, 5], 2)
# => {[1, 2], [3, 4, 5]}
# Split while
Enum.split_while([1, 2, 3, 4, 1], &(&1 < 3))
# => {[1, 2], [3, 4, 1]}
Combining
# Zip
Enum.zip([1, 2, 3], [:a, :b, :c])
# => [{1, :a}, {2, :b}, {3, :c}]
# Zip with function
Enum.zip_with([1, 2, 3], [4, 5, 6], &(&1 + &2))
# => [5, 7, 9]
# Concat
Enum.concat([[1, 2], [3, 4], [5, 6]])
# => [1, 2, 3, 4, 5, 6]
# Flat map
Enum.flat_map([1, 2, 3], fn x -> [x, x * 2] end)
# => [1, 2, 2, 4, 3, 6]
# Intersperse
Enum.intersperse([1, 2, 3], 0)
# => [1, 0, 2, 0, 3]
Uniqueness
# Uniq
Enum.uniq([1, 2, 1, 3, 2, 4]) # => [1, 2, 3, 4]
# Uniq by
Enum.uniq_by([%{id: 1, x: 1}, %{id: 1, x: 2}], fn item -> item.id end)
# => [%{id: 1, x: 1}]
# Dedup (consecutive)
Enum.dedup([1, 1, 2, 2, 3, 3, 2])
# => [1, 2, 3, 2]
Stream Module
Lazy, composable operations - no intermediate lists.
Why Stream?
# Eager (Enum) - creates intermediate lists
[1, 2, 3, 4, 5]
|> Enum.map(&(&1 * 2)) # => [2, 4, 6, 8, 10]
|> Enum.filter(&(&1 > 5)) # => [6, 8, 10]
|> Enum.sum() # => 24
# Lazy (Stream) - no intermediate lists
[1, 2, 3, 4, 5]
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 5))
|> Enum.sum() # Only now it evaluates => 24
Stream Operations
# Infinite streams
Stream.cycle([1, 2, 3]) # Repeats forever
|> Enum.take(7)
# => [1, 2, 3, 1, 2, 3, 1]
Stream.iterate(0, &(&1 + 1)) # 0, 1, 2, 3, ...
|> Enum.take(5)
# => [0, 1, 2, 3, 4]
Stream.repeatedly(fn -> :rand.uniform(10) end)
|> Enum.take(3)
# => [7, 2, 9] (random)
# Unfold - generate from state
Stream.unfold(1, fn n -> {n, n + 1} end)
|> Enum.take(5)
# => [1, 2, 3, 4, 5]
Practical Example: Large Files
# Read large file lazily
File.stream!("large_file.txt")
|> Stream.map(&String.trim/1)
|> Stream.filter(&(String.length(&1) > 0))
|> Stream.map(&String.upcase/1)
|> Enum.take(10)
# Only reads 10 lines from file
Practical Examples
Data Processing Pipeline
# Process user data
users = [
%{name: "Alice", age: 30, active: true},
%{name: "Bob", age: 25, active: false},
%{name: "Charlie", age: 35, active: true}
]
users
|> Enum.filter(& &1.active)
|> Enum.map(&%{name: &1.name, age_group: age_group(&1.age)})
|> Enum.group_by(& &1.age_group)
defp age_group(age) when age < 30, do: :young
defp age_group(_), do: :mature
Word Frequency Counter
text = "the quick brown fox jumps over the lazy dog the fox"
text
|> String.downcase()
|> String.split()
|> Enum.frequencies()
# => %{"the" => 3, "quick" => 1, "brown" => 1, ...}
Shopping Cart
cart = [
%{name: "Apple", price: 1.50, qty: 3},
%{name: "Banana", price: 0.50, qty: 6},
%{name: "Orange", price: 2.00, qty: 2}
]
total =
cart
|> Enum.map(fn item -> item.price * item.qty end)
|> Enum.sum()
# => 10.5
Exercises
- Use
Enum.mapto square all numbers in[1, 2, 3, 4, 5] - Filter odd numbers from
[1, 2, 3, 4, 5, 6, 7, 8]and sum them - Create a map from keyword list
[a: 1, b: 2, c: 3] - Given a list of maps (users with :name and :age), group by age
- Use Stream to generate first 20 fibonacci numbers
# Solutions
# 1. Square numbers
Enum.map([1, 2, 3, 4, 5], &(&1 * &1))
# => [1, 4, 9, 16, 25]
# 2. Filter and sum odds
[1, 2, 3, 4, 5, 6, 7, 8]
|> Enum.filter(&(rem(&1, 2) == 1))
|> Enum.sum()
# => 16
# 3. Keyword to map
Enum.into([a: 1, b: 2, c: 3], %{})
# => %{a: 1, b: 2, c: 3}
# 4. Group users by age
users = [%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, %{name: "Charlie", age: 30}]
Enum.group_by(users, & &1.age)
# 5. Fibonacci with Stream
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
|> Enum.take(20)
Next Steps
Continue to 06-modules-structs.md to learn about organizing code with modules, creating custom data types with structs, and implementing protocols.