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.