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

  1. Use Enum.map to square all numbers in [1, 2, 3, 4, 5]
  2. Filter odd numbers from [1, 2, 3, 4, 5, 6, 7, 8] and sum them
  3. Create a map from keyword list [a: 1, b: 2, c: 3]
  4. Given a list of maps (users with :name and :age), group by age
  5. 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.