Control your flow
Posted on July 05, 2015 by Clive in Elixir
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. 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: 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 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: 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: Allowable items for guard 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. 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. 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. 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): 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. 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: There is also a syntactically sugared version: The value of an if/unless expression is the value returned by the expression: prints “Yeah, found me”. Coincidentally, both cond and match also return the result of the expression that executed.Pattern-Matching
<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>
Guards
def <functionName>(<functionParams>) when <guardClause>, do: <functionBody>
def operation({:divide, num1, num2}) when num2 > 0 or 0 > num2, do: num1/num2
def operation({:divide, _, _}), do: raise "can't divide by zero"
Conditional Expressions
case
case <predicate> do
<clause 1> <guard> -> <function body>
<clause 2> <guard> -> <function body>
_ -> <default match>
end
cond
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
if/else (unless/else)
if true, do: "This is the truthy bit", else: "Yeah, you failed"
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
var = if true, do: "Yeah, found me", else: "Still hiding"
:io.format "~s~n", var