Playing With Fire

Exploring the web one Elixir at a time

Control your flow

There are several ways in Elixir to control which path through your application is followed based on the data that is provided or the structure/type of the data structure that is being used.

Firstly, we will start with a recap on pattern-matching, then progressing on to guards and then cover the conditional expressions: case, cond and if/else.

 

Pattern-Matching

We’ve covered pattern-matching before, so all we’ll do is recap here.

Pattern-matching is a way of using the structure of the arguments passed into a function to determine which function clause to use to process the data:

<file>mathsops.exs</file>
defmodule MathsOps do
  def operation({:plus, num1, num2}), do: num1 + num2
  def operation({:minus, num1, num2}), do: num1 - num2
end

<iex>
iex(1)> MathsOps.operation({:plus, 1, 2})
3
iex(2)> MathsOps.operation({:minus, 4, 3})
1
</iex>

As you can see from the simplistic example, the operation that is used is dependent on the atom used in the first element of the tuple. The pattern to be matched on can be arbitrarily complex, but it pays to keep the complexity to a minimum to keep things readable/understandable.

Pattern-matching though does have its limits, and this is where guards can come to the rescue…

 

Guards

Guards are boolean expressions that are evaluated prior to a function body being executed to ensure that certain preconditions are met. Guard expressions are evaluated in the order in which they are specified, so if there are several function clauses, the guards are evaluated in a top-down fashion, with the first guard that evaluates to true will allow the function clause to execute.

The syntax for this is as follows:

def <functionName>(<functionParams>) when <guardClause>, do: <functionBody>

Guard clauses are preceded by the ‘when’ keyword.

Guards have to be simple and can only use a few of the built-in functions and are limited to operate on the data passed into the function without side-effects and must evaluate to a boolean - they can however transform the data.

If all the guard expressions applied to the function clauses evaluate to false, none of the clauses execute. This may, or may not cause an error, so ensure that all eventualities are covered:

Continuing with the MathsOps file from above:

  def operation({:divide, num1, num2}) when num2 > 0 or 0 > num2, do: num1/num2
  def operation({:divide, _, _}), do: raise "can't divide by zero"

Allowable items for guard expressions:

  • true
  • false - also other constants regarded as false
  • arithmetic expressions : +, -, *, /, div, rem and binary operators
  • boolean expressions including ‘and’ and ‘or’
  • simple functions that return boolean values: hd/1, is_atom/1, is_binary/1, is_bitstring/1, is_boolean/1, is_float/1, is_function/1, is_function/2, is_integer/1, is_list/1, is_number/1, is_pid/1, is_port/1, is_record/1, is_record/2, is_reference/1, is_term/2, is_tuple/1

 

Conditional Expressions

Pattern-matching and guards are great in most situations. However sometimes there are situations when you need an alternative approach and make a decision within a function to provide flow through your program. To this end, Elixir provides three structures that help in this: case, cond and if.

Each of these return a value that can be used in your code.

 

case

The case construct, which is similar in a way to the switch language construct in imperative languages, allows pattern-matching and guards within a function, where a value needs to be evaluated against a number of different possibilities.

case <predicate> do
  <clause 1> <guard> -> <function body>
  <clause 2> <guard> -> <function body>
  _ -> <default match>
end

You would notice that there is a catch all case at the bottom of the case clause. This is like the default case in a switch statement in imperative languages.

Elixir will enforce the matching here. If the compiler determines that there are unmatched predicates possible, then it will complain. It always pays to put in a catch-all This is achieved using the _.

When a match is run, it runs top down, executing the code associated with the first match, so order matters.

 

cond

The cond construct is similar to the case construct, but it only allows for guards, not pattern-matching. The first guard that returns truthy is executed.

Using an example from Programming Elixir by Dave Thomas (Pragmatic Programmers):

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

Here the “current” variable is an integer where the rem function does a modulo operation and its checking that the passed in integer is either divisible by 3 and/or 5. If the number passed in is 3 then the first line returns falsey. The second line returns truthy and returns the string “Fizz”.

Again like the case expression, it pays to use a catch-all. In the case of cond this is true.

 

if/else (unless/else)

The if/unless construct only evaluates a single guard. They both take two things, the condition (guard) and a keyword list that contains only the keys do: and else:.

In keeping with other languages, should the guard in the if expression evaluate to truthy (for unless this is falsey), then the code in the do: block will execute, otherwise the code in the else: block executes.

As an example:

if true, do: "This is the truthy bit", else: "Yeah, you failed"

There is also a syntactically sugared version:

if true do
  "This is the truthy bit"
else
  "Yeah, you failed"
end

unless false do
  "This is the falsey bit"
else
  "Yeah, you passed"
end

The value of an if/unless expression is the value returned by the expression:

var = if true, do: "Yeah, found me", else: "Still hiding"
:io.format "~s~n", var

prints “Yeah, found me”.

Coincidentally, both cond and match also return the result of the expression that executed.