control_flow
Master Elixir control flow with comprehensive guides on if/unless, case, cond, try/catch, and with statements. Learn to write efficient and readable Elixir code.
Elixir Control Flow Structures
Understanding Elixir Control Flow
Control flow structures are fundamental to programming, allowing developers to dictate the order in which code is executed. Elixir, a dynamic, functional language designed for building scalable and maintainable applications, offers several powerful constructs for managing control flow. Mastering these is crucial for writing efficient and readable Elixir code.
Elixir's Conditional Logic: if/unless
The if
and unless
constructs in Elixir provide basic conditional execution. if
executes a block of code if a given expression evaluates to a truthy value (anything other than false
or nil
). Conversely, unless
executes a block if the expression evaluates to a falsy value.
if :something_truthy do
IO.puts "something truthy happened"
else
IO.puts "false or nil happened"
end
unless :something_truthy do
IO.puts "nil or false happened"
else
IO.puts "something truthy happened"
end
Pattern Matching with case
The case
statement is a cornerstone of Elixir's expressive power, enabling sophisticated pattern matching. It allows you to match a value against a series of patterns and execute the code associated with the first matching pattern. If no patterns match, a MatchError
is raised, which can be handled using a wildcard pattern (_
).
case 137 do
"137" -> IO.puts "I require 137 the number."
137 -> IO.puts "Ahh much better."
138 ->
IO.puts "Blocks can start on the next line as well."
end
The wildcard pattern _
is essential for ensuring that all possible outcomes are handled, preventing unexpected errors:
case {:ok, "everything went to plan"} do
{:ok, message} -> IO.puts message
{:error, message} -> IO.puts "ERROR!: #{message}"
# ⇣catchall, otherwise you'll get an error if nothing matches
_ -> IO.puts "I match everything else!"
end
case
statements can also incorporate guards for more refined pattern matching:
case 1_349 do
n when is_integer n -> IO.puts "you gave me an integer"
n when is_binary n -> IO.puts "you gave me a binary"
_ -> IO.puts "you gave me neither an integer nor binary"
end
Sequential Conditionals with cond
The cond
construct is ideal for situations where you have multiple conditions to check sequentially, similar to an if-elseif-else
chain in other languages. It evaluates conditions in order and executes the block associated with the first condition that evaluates to true. If no conditions are met, it raises a MatchError
.
cond do
false -> IO.puts "I will never run"
true -> IO.puts "I will always run"
1235 -> IO.puts "I would run if that dang true wasn't on top of me."
end
Using true
as the last condition in a cond
statement provides a convenient way to implement a default or catch-all behavior:
guess = 12
cond do
guess == 10 -> IO.puts "You guessed 10!"
guess == 46 -> IO.puts "You guessed 46!"
true ->
IO.puts "I give up."
end
Exception Handling with try/catch/after
Elixir's try
, catch
, and after
blocks offer a robust mechanism for handling exceptions and side effects. You can throw
any data type within a try
block, which can then be caught and pattern-matched in the catch
block. The after
block guarantees execution regardless of whether a throw occurred.
try do
IO.puts "Inside a try block"
throw [:hey, "Reggie"]
IO.puts "if there is a throw before me, I'll never run."
catch
x when is_number(x) -> IO.puts "!!A number was thrown."
[:hey, name] -> IO.puts "!!Hey was thrown to #{name}."
_ -> IO.puts "Something else was thrown."
after
IO.puts "I run regardless of a throw."
end
Chaining Operations with with
The with
statement is a powerful construct for chaining multiple operations that are expected to succeed. It executes a series of pattern matches sequentially. If all matches succeed, the do
block is executed. If any match fails, the non-matching value is returned, and the with
block is exited. An optional else
block can be provided to handle failures gracefully, functioning similarly to a case
statement for the failed match.
nums = [8,13,44]
# ┌left arrow ┌comma
# match left | match right |
# ┌───┴────┐ ⇣ ┌───────┴─────────┐⇣
with {:ok, num} <- Enum.fetch(nums, 2),
"44" <- Integer.to_string(num),
do: "it was 44"
# Paterns can take guards
with a when is_nil(a) <- nil,
do: "Accepts guards"
else
_ -> "Does not accept guards"
# From the docs
opts = %{width: 10, height: 15}
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height),
do: {:ok, width * height}
# returns {:ok, 150}
opts = %{width: 10}
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height),
do: {:ok, width * height}
# returns :error as that's what Map.fetch returns when a key is not present.
# ┌─ Or you can catch the error in an else block
else
:error -> "A key wasn't found!"
For more information on Elixir's control flow and functional programming paradigms, refer to the official Elixir documentation and resources like Exercism Elixir track for practice exercises.