Control Flow

Elixir provides several constructs for control flow, all based on pattern matching and expressions that return values.

if and unless

Basic if

if true do
  "This will execute"
end
# => "This will execute"

if false do
  "This won't execute"
end
# => nil

if/else

age = 18

if age >= 18 do
  "Adult"
else
  "Minor"
end
# => "Adult"

# One-liner syntax
if age >= 18, do: "Adult", else: "Minor"

unless

unless is the opposite of if - executes when condition is false.

unless is_nil(value) do
  "Value exists"
else
  "Value is nil"
end

# Equivalent to:
if is_nil(value) do
  "Value is nil"
else
  "Value exists"
end

Note: Avoid unless with else - it's confusing. Use if instead.

case

Pattern match against multiple clauses. First match wins.

Basic case

case {1, 2, 3} do
  {1, x, 3} -> 
    "Matched! x = #{x}"
  {4, 5, 6} -> 
    "Won't match"
  _ -> 
    "Default case"
end
# => "Matched! x = 2"

Multiple Patterns

result = {:ok, 42}

case result do
  {:ok, value} -> 
    "Success: #{value}"
  {:error, reason} -> 
    "Error: #{reason}"
  _ -> 
    "Unknown"
end
# => "Success: 42"

With Guards

Guards add additional conditions to patterns.

case {1, 2, 3} do
  {x, y, z} when x + y == z ->
    "x + y equals z"
  {x, y, z} when x * 2 == y ->
    "x * 2 equals y"
  _ ->
    "No match"
end
# => "No match"

Practical Example: HTTP Response

case HTTPClient.get(url) do
  {:ok, %{status: 200, body: body}} ->
    {:ok, parse_body(body)}
  
  {:ok, %{status: 404}} ->
    {:error, :not_found}
  
  {:ok, %{status: status}} ->
    {:error, {:http_error, status}}
  
  {:error, reason} ->
    {:error, {:network_error, reason}}
end

cond

Check multiple conditions (like else-if chains).

age = 25

cond do
  age < 13 -> 
    "Child"
  age < 20 -> 
    "Teenager"
  age < 65 -> 
    "Adult"
  true -> 
    "Senior"
end
# => "Adult"

Important: At least one condition must be true, or you get an error. Use true -> as a catch-all.

Practical Example: Grade Calculation

score = 85

grade = cond do
  score >= 90 -> "A"
  score >= 80 -> "B"
  score >= 70 -> "C"
  score >= 60 -> "D"
  true -> "F"
end
# => "B"

with

Chain operations that might fail, with early return on error.

Basic with

with {:ok, file} <- File.read("config.json"),
     {:ok, data} <- Jason.decode(file),
     {:ok, validated} <- validate(data) do
  {:ok, validated}
end

If any step returns something that doesn't match the pattern, that value is returned immediately.

with/else

Handle failures with specific patterns:

with {:ok, user} <- fetch_user(id),
     {:ok, posts} <- fetch_posts(user.id),
     {:ok, comments} <- fetch_comments(posts) do
  {:ok, {user, posts, comments}}
else
  {:error, :not_found} ->
    {:error, "User not found"}
  
  {:error, :database_error} ->
    {:error, "Database connection failed"}
  
  error ->
    {:error, "Unknown error: #{inspect(error)}"}
end

Practical Example: User Registration

def register_user(params) do
  with {:ok, validated} <- validate_params(params),
       {:ok, hashed_password} <- hash_password(validated.password),
       {:ok, user} <- insert_user(validated, hashed_password),
       {:ok, _email} <- send_welcome_email(user) do
    {:ok, user}
  else
    {:error, :invalid_email} ->
      {:error, "Email format is invalid"}
    
    {:error, :weak_password} ->
      {:error, "Password too weak"}
    
    {:error, :email_taken} ->
      {:error, "Email already registered"}
    
    error ->
      Logger.error("Registration failed: #{inspect(error)}")
      {:error, "Registration failed"}
  end
end

Loops and Recursion

Elixir has no traditional loops - use recursion or Enum module.

Recursion Pattern

defmodule Counter do
  def count_down(0), do: IO.puts("Done!")
  
  def count_down(n) do
    IO.puts(n)
    count_down(n - 1)
  end
end

Counter.count_down(5)
# 5
# 4
# 3
# 2
# 1
# Done!

List Processing with Recursion

defmodule ListSum do
  def sum([]), do: 0
  def sum([head | tail]), do: head + sum(tail)
end

ListSum.sum([1, 2, 3, 4, 5])
# => 15

Tail Recursion (Optimized)

Use accumulator for better performance:

defmodule ListSum do
  def sum(list), do: sum(list, 0)
  
  defp sum([], acc), do: acc
  defp sum([head | tail], acc), do: sum(tail, acc + head)
end

ListSum.sum([1, 2, 3, 4, 5])
# => 15

Enum Module (Preferred for Loops)

Instead of writing recursion, use Enum:

each - Iterate

Enum.each([1, 2, 3], fn x -> 
  IO.puts(x) 
end)
# 1
# 2
# 3

map - Transform

Enum.map([1, 2, 3], fn x -> x * 2 end)
# => [2, 4, 6]

# Shorthand
Enum.map([1, 2, 3], &(&1 * 2))

filter - Select

Enum.filter([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)
# => [2, 4]

reduce - Accumulate

Enum.reduce([1, 2, 3, 4, 5], 0, fn x, acc -> x + acc end)
# => 15

# With & shorthand
Enum.reduce([1, 2, 3, 4, 5], 0, &+/2)

Chaining Operations

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|> Enum.filter(&(rem(&1, 2) == 0))  # Keep evens
|> Enum.map(&(&1 * &1))              # Square
|> Enum.take(3)                      # First 3
# => [4, 16, 36]

Common Enum Functions

# 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

# find - Get first match
Enum.find([1, 2, 3, 4], &(&1 > 2))  # => 3

# count
Enum.count([1, 2, 3])  # => 3

# member?
Enum.member?([1, 2, 3], 2)  # => true

# at - Get element at index
Enum.at([1, 2, 3], 1)  # => 2

# zip - Combine two lists
Enum.zip([1, 2, 3], [:a, :b, :c])
# => [{1, :a}, {2, :b}, {3, :c}]

Comprehensions

Syntactic sugar for mapping and filtering:

Basic for

for n <- [1, 2, 3, 4], do: n * 2
# => [2, 4, 6, 8]

With Filter

for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
# => [2, 4, 6]

Multiple Generators

for x <- [1, 2], y <- [3, 4], do: {x, y}
# => [{1, 3}, {1, 4}, {2, 3}, {2, 4}]

Pattern Matching in Generator

users = [
  %{name: "Alice", age: 30},
  %{name: "Bob", age: 25},
  %{name: "Charlie", age: 35}
]

for %{name: name, age: age} <- users, age > 26, do: name
# => ["Alice", "Charlie"]

Into Different Collections

# Into a map
for {k, v} <- [a: 1, b: 2, c: 3], into: %{}, do: {k, v * 2}
# => %{a: 2, b: 4, c: 6}

# Into a binary (string)
for c <- 'hello', into: "", do: <<c + 1>>
# => "ifmmp"

Practical Example: Nested Iteration

# Multiplication table
for x <- 1..10, y <- 1..10, do: {x, y, x * y}

# Filter coordinates
for x <- 1..5, y <- 1..5, x + y <= 6, do: {x, y}

Guards

Additional conditions in pattern matching:

Guard Clauses

defmodule Math do
  def abs(n) when n < 0, do: -n
  def abs(n) when n >= 0, do: n
end

Math.abs(-5)  # => 5
Math.abs(5)   # => 5

Multiple Guards

defmodule Classify do
  def number_type(n) when is_integer(n) and n < 0, do: :negative_integer
  def number_type(n) when is_integer(n) and n > 0, do: :positive_integer
  def number_type(n) when n == 0, do: :zero
  def number_type(n) when is_float(n), do: :float
end

Allowed in Guards

Only certain expressions are allowed in guards:

# Comparison operators: ==, !=, ===, !==, <, <=, >, >=
# Boolean operators: and, or, not
# Arithmetic: +, -, *, /
# Type checks: is_integer, is_float, is_atom, is_list, etc.
# Other: length, hd, tl, elem, etc.

Not allowed: Your own functions, most standard library functions

Practical Examples

FizzBuzz

defmodule FizzBuzz do
  def run(n) do
    1..n
    |> Enum.map(&fizzbuzz/1)
    |> Enum.each(&IO.puts/1)
  end
  
  defp fizzbuzz(n) when rem(n, 15) == 0, do: "FizzBuzz"
  defp fizzbuzz(n) when rem(n, 3) == 0, do: "Fizz"
  defp fizzbuzz(n) when rem(n, 5) == 0, do: "Buzz"
  defp fizzbuzz(n), do: n
end

FizzBuzz.run(15)

Processing API Response

def process_response(response) do
  case response do
    {:ok, %{status: 200, body: body}} ->
      case Jason.decode(body) do
        {:ok, data} -> transform_data(data)
        {:error, _} -> {:error, :invalid_json}
      end
    
    {:ok, %{status: status}} when status in 400..499 ->
      {:error, :client_error}
    
    {:ok, %{status: status}} when status in 500..599 ->
      {:error, :server_error}
    
    {:error, reason} ->
      {:error, {:network, reason}}
  end
end

# Better with `with`:
def process_response(response) do
  with {:ok, %{status: 200, body: body}} <- response,
       {:ok, data} <- Jason.decode(body) do
    transform_data(data)
  else
    {:ok, %{status: status}} when status in 400..499 ->
      {:error, :client_error}
    
    {:ok, %{status: status}} when status in 500..599 ->
      {:error, :server_error}
    
    error ->
      {:error, error}
  end
end

Validation Pipeline

def validate_user(params) do
  with :ok <- validate_email(params[:email]),
       :ok <- validate_password(params[:password]),
       :ok <- validate_age(params[:age]) do
    {:ok, params}
  else
    {:error, reason} -> {:error, reason}
  end
end

defp validate_email(email) when is_binary(email) do
  if String.contains?(email, "@"), do: :ok, else: {:error, :invalid_email}
end
defp validate_email(_), do: {:error, :invalid_email}

defp validate_password(pass) when is_binary(pass) do
  if String.length(pass) >= 8, do: :ok, else: {:error, :weak_password}
end
defp validate_password(_), do: {:error, :invalid_password}

defp validate_age(age) when is_integer(age) and age >= 18 do
  :ok
end
defp validate_age(_), do: {:error, :invalid_age}

Exercises

  1. Write a function using case that takes a tuple {:ok, value} or {:error, reason} and returns the value or a default
  2. Use cond to categorize a temperature: "Freezing" (< 0), "Cold" (0-15), "Mild" (16-25), "Hot" (> 25)
  3. Write a recursive function to calculate factorial
  4. Use comprehension to generate pairs of {x, y} where x and y are from 1-5 and x < y
  5. Use with to safely chain three operations that might fail
# Solutions

# 1. Case with default
def unwrap(result, default) do
  case result do
    {:ok, value} -> value
    {:error, _} -> default
  end
end

# 2. Temperature cond
def categorize_temp(temp) do
  cond do
    temp < 0 -> "Freezing"
    temp <= 15 -> "Cold"
    temp <= 25 -> "Mild"
    true -> "Hot"
  end
end

# 3. Factorial
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)

# 4. Comprehension
for x <- 1..5, y <- 1..5, x < y, do: {x, y}

# 5. With chain
with {:ok, user} <- get_user(id),
     {:ok, posts} <- get_posts(user),
     {:ok, sorted} <- sort_posts(posts) do
  {:ok, {user, sorted}}
end

Next Steps

Continue to 04-functions.md to learn about anonymous functions, named functions, function clauses, guards, and the powerful pipe operator.