Elixir code tries to be declarative, not imperative.
We write lots of small functions, and a combination of guard clauses and pattern matching of parameters replaces most of the control flow seen in other languages.
Always consider a more functional alternative than using some of these structures.
The benefit will become obvious as you write more code—functions written without explicit control flow tend to be shorter and more focused.
They’re easier to read, test, and reuse.
if and unless, take two parameters:
a condition and a keyword list, which can contain the keys do: and else:.
iex> if 1 == 1, do: "true part", else: "false part"
"true part"
iex> if 1 == 2, do: "true part", else: "false part"
"false part"
iex> if 1 == 1 do
...> "true part"
...> else
...> "false part"
...> end
"true part"
iex> unless 1 == 1, do: "error", else: "OK"
"OK"
iex> unless 1 == 2, do: "OK", else: "error"
"OK"
iex> unless 1 == 2 do
...> "OK"
...> else
...> "error"
...> end
"OK"
The cond macro lets you list out a series of conditions, each with associated code. It executes the code corresponding to the first truthy conditions.
defmodule FizzBuzz do
def upto(n) when n > 0, do: _upto(1, n, [])
defp _upto(_current, 0, result), do: Enum.reverse result
defp _upto(current, left, result) do
next_answer =
cond do
rem(current, 3) == 0 and rem(current, 5) == 0 ->
"FizzBuzz"
rem(current, 3) == 0 ->
"Fizz"
rem(current, 5) == 0 ->
"Buzz"
true ->
current
end
_upto(current+1, left-1, [ next_answer | result ])
end
end
The last example we had to reverse when we were done.
Let's try it by processing the numbers in reverse order.
defmodule FizzBuzz do
def upto(n) when n > 0, do: _downto(n, [])
defp _downto(0, result), do: result
defp _downto(current, result) do
next_answer =
cond do
rem(current, 3) == 0 and rem(current, 5) == 0 ->
"FizzBuzz"
rem(current, 3) == 0 ->
"Fizz"
rem(current, 5) == 0 ->
"Buzz"
true ->
current
end
_downto(current-1, [ next_answer | result ])
end
end
Let’s use Enum.map to transform the range of numbers from 1 to n to the corresponding FizzBuzz words.
defmodule FizzBuzz do
def upto(n) when n > 0 do
1..n |> Enum.map(&fizzbuzz/1)
end
defp fizzbuzz(n) do
cond do
rem(n, 3) == 0 and rem(n, 5) == 0 ->
"FizzBuzz"
rem(n, 3) == 0 ->
"Fizz"
rem(n, 5) == 0 ->
"Buzz"
true ->
n
end
end
end
FizzBuss without cond.
defmodule FizzBuzz do
def upto(n) when n > 0, do: 1..n |> Enum.map(&fizzbuzz/1)
defp fizzbuzz(n), do: _fizzword(n, rem(n, 3), rem(n, 5))
defp _fizzword(_n, 0, 0), do: "FizzBuzz"
defp _fizzword(_n, 0, _), do: "Fizz"
defp _fizzword(_n, _, 0), do: "Buzz"
defp _fizzword( n, _, _), do: n
end
case lets you test a value against a set of patterns, executes the code associated with the first one that matches, and returns the value of that code.
The patterns may include guard clauses.
case File.open("case.ex") do
{ :ok, file } ->
IO.puts "First line: #{IO.read(file, :line)}"
{ :error, reason } ->
IO.puts "Failed to open file: #{reason}"
end
Lets use nested pattern matches.
defmodule Users do
dave = %{ name: "Dave", state: "TX", likes: "programming" }
case dave do
%{state: some_state} = person ->
IO.puts "#{person.name} lives in #{some_state}"
_ ->
IO.puts "No matches"
end
end
Adding guard clauses with case.
defmodule Bouncer do
dave = %{name: "Dave", age: 27}
case dave do
person = %{age: age} when is_number(age) and age >= 21 ->
IO.puts "You are cleared to enter the Foo Bar, #{person.name}"
_ ->
IO.puts "Sorry, no admission"
end
end
First, the official warning:
exceptions in Elixir are not control-flow structures.
Elixir exceptions are intended for things that should never happen in normal operation.
Failing to open a configuration file whose name is fixed could be seen as exceptional. However, failing to open a file whose name a user entered is not.
Raise an exception with the raise function. At its simplest, you pass it a string and it generates an exception of type RuntimeError.
iex> raise "Giving up"
** (RuntimeError) Giving up
# pass the type of the exception, along with other optional attributes.
iex> raise RuntimeError
** (RuntimeError) runtime error
iex> raise RuntimeError, message: "override message"
** (RuntimeError) override message
You use exceptions far less in Elixir
than in other languages.
The design philosophy is that errors should propagate back up to an external, supervising process.
How to handle opening a file:
# catch the failure case
case File.open(user_file_name) do
{:ok, file} ->
process(file)
{:error, message} ->
IO.puts :stderr, "Couldn't open #{user_file_name}: #{message}"
end
# raise exception on failure
case File.open("config_file") do
{:ok, file} ->
process(file)
{:error, message} ->
raise "Failed to open config file: #{message}"
end
# or let Elixir raise the exception
{ :ok, file } = File.open("config_file")
process(file)
# let File.open! throw the exception for you
file = File.open!("config_file")
Elixir has just a few forms of control flow:
if, unless, cond, case, and (perhaps) raise.
But surprisingly, this doesn’t matter in practice.
Elixir programs are rich and expressive without a lot of branching code.
And they’re easier to work with as a result.