Erlang and Elixir, Part 4: Control Flow

If, else, and logical operators are probably the most common similarity between the popular languages, and unsurprisingly Elixir has them, too. The if, case, and cond macros are provided for giving us control flow structure.

Erlang and Elixir, Part 4: Control Flow

For pattern matching, we mentioned before that using case provides matches against any pattern iteratively:

iex> checkUser = 'simon'
'simon'

iex> case {checkUser} do
...>      {'simon'} ->
...>           'User Match - Simon'
...>      {'mary'} ->
...>           'User Match - Mary' 
...>      _ ->
...>           'This will match any value.. use as a catch-all'
...> end

'User Match - Simon'

Worth noticing here is the _ case, which is essentially a default catch-all case. The case can be also used with an atom or any variable type like so.

iex> case {:user} do
...>      {:user} ->
...>           'User Match'
...>      _ ->
...>           'No match'
...> end

'User Match'

Guard Clause Expressions

Elixir provides many operators to check in our expression as a guard against catching the wrong data. By default, the following are supported:

  • comparison operators (==!====!==>>=<<=)
  • boolean operators (andornot)
  • arithmetic operations (+-*/)
  • arithmetic unary operators (+-)
  • the binary concatenation operator <>
  • the in operator as long as the right side is a range or a list
  • all the following type check functions:
    • 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_map/1
    • is_nil/1
    • is_number/1
    • is_pid/1
    • is_port/1
    • is_reference/1
    • is_tuple/1
  • plus these functions:
    • abs(number)
    • binary_part(binary, start, length)
    • bit_size(bitstring)
    • byte_size(bitstring)
    • div(integer, integer)
    • elem(tuple, n)
    • hd(list)
    • length(list)
    • map_size(map)
    • node()
    • node(pid | ref | port)
    • rem(integer, integer)
    • round(number)
    • self()
    • tl(list)
    • trunc(number)
    • tuple_size(tuple)

We can use them like so:

iex> case 1 do                     
...>   x when is_number(x) -> "Number #{x}"
...>   x when is_boolean(x) -> "Boolean #{x}"
...> end
"Number 1"

Additionally, an anonymous function can have multiple guards. For example, to calculate a pivot point from a financial market data using the high, low, and close values, we can do this:

iex> pivot = fn
...>         h, l, c when h < l -> "Error"      
...>         h, l, c -> (h + l + c) / 3
...> end
iex> pivot.(1233, 1212, 1226) # Usage: High, Low, Close..
1223.6666666666667

Here, if the high value is less than low, the pivot anonymous function will return an Error message. This one-line fashion of writing guards is phenomenally powerful.

Cond

Like a switch statement, Elixir’s cond is where we can perform a string of if else like blocks and execute on the match.

iex> cond do
...>   2 + 2 == 5 ->
...>     "This is never true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   true ->
...>     "This is always true (equivalent to else)"
...> end
"This is always true (equivalent to else)

Inside a cond block, everything is evaluating to true except nil or false. That’s all numerical values and strings included.

For example, we can check the current status for various possibilities with a cond block:

iex> cond do
...>     status == 'available' ->
...>        "User is available"
...>     status == 'busy' ->
...>       "User is busy"
...>     true ->
...>       "No input provided"
...> end

When the value of status is set to a string, it will evaluate; otherwise, an error message will be output by default via the last true case.

If and Unless

Elixir also provides us with unless, which is a default part of the if else block and can be demonstrated as so:

iex> if true do 
...>  'works'
...> end
'works'
iex> unless false do
...> 'other case'
...> end
'other case'

When the if case evaluates as true, unless will allow you to have access to the opposite. As demonstrated, we can catch the other side of the conditional this way.

In a real-life example, this can be used for displaying conditional information during a conditional check, such as a reminder or hint.

iex> if t do
...>   'Process information here...'
...> end
nil
iex> unless false do
...>    'Hint to show user when information is not right' 
...> end
'Hint to show user when information is not right'

Do/End Blocks

Throughout these examples, we have seen the usage of the do and end macros. Let’s have a closer look at them now before we conclude this section:

iex> if true, do: 1 + 2
3

The comma after true is for Elixir’s regular syntax, where each argument is comma-separated. This syntax uses keyword lists, and when used in conjunction with the else macro it will look like this:

iex> if false, do: :this, else: :that
:that

To make this nicer for development, we can use the do/end block, which does not require the comma for a direct approach:

iex> if true do
...>   a = 1 
...>   a + 10
...> end
11
iex> if true, do: (
...>   a = 1
...>   a + 10
...> )
11

Conclusion

Elixir has the familiar control flow structures if/else and case, and it also has unless and the do/end block.

We can achieve any logic via manipulation of control flow with these structures. When you use them in conjunction with macros, anonymous functions, and modules, you have a great supply of options to get the work done.

For continued reading on the topic, the manual provides more insights into some of the further caveats of using the do/end block, such as function scope limitations.