DEV Community

Cover image for Learning Elixir: Control Flow with If and Unless
João Paulo Abreu
João Paulo Abreu

Posted on

Learning Elixir: Control Flow with If and Unless

Control structures are fundamental building blocks in Elixir, with if and unless being the most basic forms of flow control. Understanding how these structures work as expressions rather than statements is crucial for writing idiomatic Elixir code.

Note: The examples in this article use Elixir 1.17.3. While most operations should work across different versions, some functionality might vary.

Table of Contents

Introduction

In Elixir, if and unless are expressions that return values, not statements that control program flow. This is a fundamental difference from languages like JavaScript or Python where if is a statement. For example:

In Elixir, if is an expression that returns a value:

iex> age = 19
19
iex> result = if age >= 18, do: "adult", else: "minor"
"adult"
Enter fullscreen mode Exit fullscreen mode

This is equivalent to this JavaScript code:

let age = 19;
let result;
if (age >= 18) {
    result = "adult";
} else {
    result = "minor";
}
Enter fullscreen mode Exit fullscreen mode

Understanding If Expressions

Basic If Syntax

# Single line if
iex> if true, do: "this", else: "that"
"this"

# Multi-line if
# Copy and test the following code in IEx:
if true do
  "this"
else
  "that"
end

# If without else
iex> if false, do: "this"
nil
Enter fullscreen mode Exit fullscreen mode

Truthy and Falsy Values

# Only false and nil are falsy
iex> if false, do: "won't print"
nil
iex> if nil, do: "won't print"
nil

# Everything else is truthy
iex> if 0, do: "zero is truthy"
"zero is truthy"
iex> if [], do: "empty list is truthy"
"empty list is truthy"
Enter fullscreen mode Exit fullscreen mode

If as an Expression

# Assigning if results
iex> result = if 5 > 3, do: "greater", else: "lesser"
"greater"

# Using in function calls
iex> String.upcase(if true, do: "hello", else: "world")
"HELLO"

# In pipeline operations
iex> 5 |> if(do: "positive", else: "negative")
"positive"

# While possible, using if in pipelines is not recommended
# Instead, prefer more readable alternatives like case:
"test"
|> String.upcase()
|> case do
  "TEST" -> "yes"
  _ -> "no"
end
Enter fullscreen mode Exit fullscreen mode

Unless Expressions

Basic Unless Syntax

# Single line unless
iex> unless false, do: "this", else: "that"
"this"

# Multi-line unless
# Copy and test the following code in IEx:
unless false do
  "this"
else
  "that"
end

# Unless without else
iex> unless true, do: "won't print"
nil
Enter fullscreen mode Exit fullscreen mode

Unless vs If Not

# These are equivalent
# First, let's set a value for age
iex> age = 17
17
# Now let's test different ways to check if someone is an adult
iex> unless age < 18, do: "adult", else: "minor"
"minor"
iex> if not(age < 18), do: "adult", else: "minor"
"minor"
iex> if age >= 18, do: "adult", else: "minor"
"minor"
Enter fullscreen mode Exit fullscreen mode

Common Use Cases

Error Handling

# Simple validation
defmodule Validator do
  def validate_age(age) do
    if age >= 0 do
      {:ok, age}
    else
      {:error, "Age cannot be negative"}
    end
  end

  def ensure_positive(number) do
    unless number <= 0 do
      {:ok, number}
    else
      {:error, "Number must be positive"}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Conditional Computation

defmodule Calculator do
  def safe_divide(num, denominator) do
    if denominator != 0 do
      {:ok, num / denominator}
    else
      {:error, "Cannot divide by zero"}
    end
  end

  def compute_discount(price, quantity) do
    if quantity >= 10 do
      price * 0.9  # 10% discount
    else
      price
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Authorization Checks

defmodule Auth do
  def process_request(user, action) do
    if authorized?(user, action) do
      perform_action(action)
    else
      {:error, :unauthorized}
    end
  end

  defp authorized?(user, action) do
    # Authorization logic here
    user.role in [:admin, :manager]
  end

  defp perform_action(action) do
    # Action execution logic
    {:ok, "Performed #{action}"}
  end
end
Enter fullscreen mode Exit fullscreen mode

Combining with Pattern Matching

Pattern Matching in Conditions

# Pattern matching in the condition
# This module shows how to use pattern matching within if conditions
defmodule Matcher do
  # Using case is more idiomatic for pattern matching
  def process_response(response) do
    case response do
      {:ok, value} -> "Got value: #{value}"
      _ -> "Invalid response"
    end
  end

  # Pattern matching in function heads is clearer than if
  def handle_user(%{role: role}) when role in [:admin, :manager] do
    "Authorized user"
  end
  def handle_user(_user) do
    "Unauthorized user"
  end
end

# Test the Matcher module:
iex> Matcher.process_response({:ok, "test"})
"Got value: test"
iex> Matcher.process_response({:error, "oops"})
"Invalid response"
iex> Matcher.handle_user(%{role: :admin})
"Authorized user"
iex> Matcher.handle_user(%{role: :guest})
"Unauthorized user"
Enter fullscreen mode Exit fullscreen mode

Using with Guards

defmodule Guard do
  def process_number(num) when is_integer(num) do
    if num > 0 do
      {:ok, num * 2}
    else
      {:error, "Number must be positive"}
    end
  end

  def process_string(str) when is_binary(str) do
    unless String.length(str) == 0 do
      {:ok, String.upcase(str)}
    else
      {:error, "String cannot be empty"}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Avoiding Deep Nesting

While if statements are useful for simple conditions, nesting multiple if statements can make code harder to read and maintain. Elixir provides better tools like cond, case, and pattern matching for handling complex conditions. Here's an example comparing a deeply nested approach with a cleaner alternative:

# Copy and test the following code in IEx:
defmodule FlowControl do
  # This is an example of deeply nested if statements - avoid this pattern
  def deep_nest(x, y, z) do
    if x > 0 do
      if y > 0 do
        if z > 0 do
          x + y + z
        else
          0
        end
      else
        0
      end
    else
      0
    end
  end

  # This is a cleaner way to write the same logic using cond
  def flat_logic(x, y, z) do
    cond do
      x <= 0 -> 0
      y <= 0 -> 0
      z <= 0 -> 0
      true -> x + y + z
    end
  end
end

# Test the different approaches:
iex> FlowControl.deep_nest(1, 2, 3)
6
iex> FlowControl.deep_nest(1, 0, 3)
0
iex> FlowControl.flat_logic(1, 2, 3)
6
iex> FlowControl.flat_logic(1, 0, 3)
0
Enter fullscreen mode Exit fullscreen mode

Best Practices for Using If and Unless

When working with conditional logic in Elixir, consider these guidelines:

Use if and unless for Simple Conditions

  • Single conditions that are easy to read
  • When the logic is straightforward and doesn't require pattern matching
# When the positive condition is clearer:
if user.age >= 18 do
  allow_access()
end

# When the negative condition is clearer:
unless user.verified? do
  raise "Account not verified"
end 
Enter fullscreen mode Exit fullscreen mode

Prefer Pattern Matching When Possible

# Instead of
def process(response) do
 if match?({:ok, _}, response) do
   {:ok, value} = response
   value
 end
end

# Prefer
def process({:ok, value}), do: value
def process(_), do: nil
Enter fullscreen mode Exit fullscreen mode

Conclusion

if and unless in Elixir are useful expressions that form the foundation of control flow. Understanding their nature as expressions rather than statements is key to writing idiomatic Elixir code. Through this guide, we've explored:

  • The fundamental behavior of if and unless as expressions
  • Common use cases and patterns
  • Integration with pattern matching and guards

Remember that while if and unless are useful, Elixir often provides more elegant solutions through pattern matching and multi-clause functions.

Tip: When writing conditional logic, first consider if pattern matching could provide a clearer solution before reaching for if or unless.

Further Reading

Next Steps

In the upcoming article, we'll explore Case and Cond Structures:

Case and Cond Structures

  • Pattern matching in case expressions
  • Multiple conditions with cond
  • Combining case and cond effectively
  • Best practices and common patterns

Top comments (0)