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
- Understanding If Expressions
- Unless Expressions
- Common Use Cases
- Combining with Pattern Matching
- Best Practices for Using If and Unless
- Conclusion
- Further Reading
- Next Steps
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"
This is equivalent to this JavaScript code:
let age = 19;
let result;
if (age >= 18) {
result = "adult";
} else {
result = "minor";
}
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
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"
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
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
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"
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
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
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
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"
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
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
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
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
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
andunless
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
orunless
.
Further Reading
- Elixir School - Control Structures
- Elixir Documentation - Kernel.if/2
- Elixir Documentation - Kernel.unless/2
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)