Elixir 1.6

Chapter 12 

Control Flow

Control Flow

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

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"

cond

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

cond

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

cond

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

cond

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

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

case

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

case

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

Raising Exceptions

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.

Raising Exceptions

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.

Designing with Exceptions

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"​)

Doing More with Less

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.

Thank you!

Programming Elixir 1.6 Chapter 12

By Dustin McCraw

Programming Elixir 1.6 Chapter 12

  • 934