Guards, Guards!

Guards in Elixir (and Erlang)

Marten / Qqwy

Guards, Guards!

Guards in Elixir (and Erlang)

Marten / Qqwy

• Guards are specific to Erlang/Elixir:
• Design choices: Riding the line between efficiency/simplicity $$\leftrightarrow$$ flexibility
• Implications not immediately obvious to newcomers

I'll be mostly showing Elixir code

Summary

1. What are guards, exactly?
2. Case, Cond, If, ... when use what?
3. Custom guards & guard-aware macros
4. How to go overboard with writing guard-safe code

Conclusion

Summary

1. What are guards, exactly?
2. Case, Cond, If, ... when use what?
3. Custom guards & guard-aware macros
4. How to go overboard with writing guard-safe code

Conclusion

1. What are guards, exactly?

What is Pattern-Matching, exactly?

$$\rightarrow$$ Extension to pattern-matching

1. What are guards, exactly?

What is Pattern-Matching, exactly?

$$\rightarrow$$ Extension to pattern-matching

$$\rightarrow$$ Powerful conditional programming construct

def list_length(list) do
case list do
[]  ->
0
[ _ | tail ] ->
1 + list_length(tail)
end
end

"Powerfulness"

of programming abstractions

How powerful is pattern-matching?

?! Not possible with just a pattern

How powerful is pattern-matching?

+ guards

"extra boolean checks"

1. What are guards, exactly?

def abs(x) when x <  0, do: -x
def abs(x) when x >= 0, do:  x

1. What are guards, exactly?

def sign(x) when x == 0, do: 0
def sign(x) when x > 0,  do: 1
def sign(x) when x < 0,  do: -1
def abs(x) when x <  0, do: -x
def abs(x) when x >= 0, do:  x

Guards are allowed (essentially) anywhere patterns are allowed

• function clauses
• case expressions
• anonymous functions
• for and with
• try
• match?
• not allowed in =!

Only some functions are allowed in a guard

• comparison operators(==, !=, <, >, <=, >=, ===, !==)
• some arithmetic operations (+, -, *, /, div, rem, abs, round, ...)
• is_* "type-check" functions
• some functions to check sizes or convert tuples, lists, maps, strings
• (strict) boolean operators (and, or, not)

But...

And nothing else!

Only some functions are allowed in a guard

• comparison operators(==, !=, <, >, <=, >=, ===, !==)
• some arithmetic operations (+, -, *, /, div, rem, abs, min, max, round, ...)
• is_* "type-check" functions
• some functions to check sizes or convert tuples, lists, maps, strings
• (strict) boolean operators (and, or, not)

But...

And nothing else!

$$\leftarrow$$ only added later!

... why?

Why not allow any user-written code?

Problems with allowing arbitrary code:

• What if it's slow?
• What if it writes to disk, calls a server, ... ?
• Pattern-matching inside a pattern-match?

Problems with allowing arbitrary code:

• What if it's slow?
• What if it writes to disk, calls a server, ... ?
• Pattern-matching inside a pattern-match?

Solution: Restrict to a tiny subset of Built-In Functions! ('BIFs')

Side note: Guards and exceptions

• Some guards do 'throw exceptions'

• Immediately makes whole guard fail

• Problem when making more complex guards

• Special syntax was introduced

def empty?(val)
when map_size(val) == 0
when tuple_size(val) == 0,
do: true
def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0 do
true
end

Patterns with guards:

• "Idiomatic"...
• ...but limited

Some things you cannot do:

• more complicated math: $$2^x, \sqrt{x}, \sin{x}, \log{x}$$
• nested patterns
• 'full' support for custom datatypes

Side note: Other languages made a different choice

• Haskell: allows arbitrary guards (guaranteed to be 'pure')
• F#: any code (even impure)

"Originally when they were just simple tests the difference between guards and normal expressions was greater and more distinct. When guards were extended with full boolean expressions (which in itself is was probably a Good Thing) this difference became much less distinct and guards then became seen as restricted normal expressions, which they are not. This has caused problems.

~Robert Virding

case

• built-in 1:1 the same between Erlang $$\leftrightarrow$$ Elixir
• (only) accepts patterns + guards
def sign(x) do
case x do
x when x == 0 ->  0
x when x > 0  ->  1
x when x < 0  -> -1
end
end
• not flexible

multiple function clauses

• Identical to using case
def sign(x) when x == 0, do: 0
def sign(x) when x > 0,  do: 1
def sign(x) when x < 0,  do: -1
def sign(x) do
case x do
x when x == 0 ->  0
x when x > 0  ->  1
x when x < 0  -> -1
end
end

if...else

• Compiles to
• allows any expression: flexible
case expression do
true ->
IO.puts("true")
false ->
IO.puts("false")
end
if expression do
IO.puts("true!")
else
IO.puts("false")
end

(also: unless)

if...else

• Compiles to
• allows any expression: flexible
case expression do
result not in [nil, false] ->
IO.puts("true")
_ ->
IO.puts("false")
end
if x do
IO.puts("true!")
else
IO.puts("false")
end

Cannot always be optimized

(Elixir is "truthy")

(also: unless)

cond

• Compiles to nested case
• Allows any expression: flexible
• Use this when you have multiple alternatives
def sign(x) do
cond do
x == 0 ->  0
x > 0  ->  1
x < 1  -> -1
end
end
def sign(x) do
case x == 0 do
true -> 0
false ->
case x > 0 do
true -> 1
false ->
case x < 0 do
true -> -1
false -> raise CondClauseError
end
end
end
end

cond

• Compiles to nested case
• Allows any expression: flexible
• Use this when you have multiple alternatives
def sign(x) do
cond do
x == 0 ->  0
x > 0  ->  1
x < 1  -> -1
end
end
def sign(x) do
case x == 0 do
true -> 0
false ->
case x > 0 do
true -> 1
false ->
case x < 0 do
true -> -1
false -> raise CondClauseError
end
end
end
end
def sign(x) do
case x == 0 do
result not in [false, nil] -> 0
_ ->
case x > 0 do
result not in [false, nil] -> 1
_ ->
case x < 0 do
result not in [false, nil] -> -1
_ -> raise CondClauseError
end
end
end
end

Side Note: Erlang also has an if

• works similar to cond

However... Elixir does not use it, because:

• Elixir uses both false and nil as non-true
• Erlang's if only allows guard clauses

When to choose what?

Which implementation is faster?

def guard_sign(x) when x == 0, do: 0
def guard_sign(x) when x > 0,  do: 1
def guard_sign(x) when x < 0,  do: -1
def case_sign(x) do
case x do
x when x == 0 -> 0
x when x > 0  -> 1
x when x < 0  -> -1
end
end
def cond_sign(x) do
cond do
x == 0 -> 0
x > 0  -> 1
x < 1  -> -1
end
end
def if_sign(x) do
if x == 0 do
0
else if x > 0 do
1
else
-1
end
end
end

Let's look at the generated BEAM bytecode!

{:function, ____________, 1, 21,
[
...
{:label, 21},
{:test, :is_eq, {:f, 22}, [x: 0, integer: 0]},
{:move, {:integer, 0}, {:x, 0}},
:return,
{:label, 22},
{:test, :is_lt, {:f, 23}, [integer: 0, x: 0]},
{:move, {:integer, 1}, {:x, 0}},
:return,
{:label, 23},
{:move, {:integer, -1}, {:x, 0}},
:return
]}

Which implementation is this?

{:function, ____________, 1, 21,
[
...
{:label, 21},
{:test, :is_eq, {:f, 22}, [x: 0, integer: 0]},
{:move, {:integer, 0}, {:x, 0}},
:return,
{:label, 22},
{:test, :is_lt, {:f, 23}, [integer: 0, x: 0]},
{:move, {:integer, 1}, {:x, 0}},
:return,
{:label, 23},
{:move, {:integer, -1}, {:x, 0}},
:return
]}

Which implementation is this?

Disclaimer: this was a tiny function

Optimizing Compilers are awesome!

• "Premature optimization is the root of all evil"
• Do not guess for your compiler
• Compiler might either be more clever
• ... or possibly not understand at all

Check, Profile and Benchmark if you want to be sure!

Takeaway:

defguard

• macro that expands to more complex guard at compile-time
• Also does compile-time sanity-checks!
defguard is_cool(number) when number == 42 or number != 13
case {:ok, 10} do
:ok -> "Yay!"
{:ok, _} -> "Yay!"
{:ok, _, _} -> "Yay!"
# ...
_ -> "Failure"
end
defguard is_ok(x)  when x == :ok or (is_tuple(x) and tuple_size(x) >= 1 and elem(x, 0) == :ok)
case {:ok, 10} do
res when is_ok(res) -> "Yay!"
_ -> "Failure"
end

What if defguard is not enough?

• Different implementation for guard and 'normal' code?
• Support more datatypes in 'normal' code
• Have simpler (or maybe more efficient) non-guard implementation.
• Usage example: Numbers operators

__CALLER__

defmacro a + b do
case __CALLER__.context do
nil ->
quote do
YourFancyImplementation.plus(a, b)
end
:guard ->
quote do
Kernel.+(a, b)
end
# :match is also available
end
end

__CALLER__.context

of working with custom datatypes

• Not perfect, but hey, it's something :-)

Takeaway:

What is mod?

• Remainder of division (similar to rem)
• But: result is positive as long as divisor is positive
• Used as precondition in many common algorithms
• Not built-in in Erlang!
mod(dividend, divisor)

Integer.mod/2 (not guard-safe)

mod(a, n) === a - (n * floor_div(a, n))
• Problem: div is not floored division!
• So how to write floor_div(a, b)?

Solution:

a | n | rem(a, n) | div(a,n) + ?
+ | + |  0        |  0
+ | + | !0        |  0
- | + |  0        |  0
- | + | !0        | -1
+ | - |  0        |  0
+ | - | !0        | -1
- | - |  0        |  0
- | - | !0        |  0
0 | + |  0        |  0
0 | + | !0        |  0
0 | - |  0        |  0
0 | - | !0        |  0
mod(a, n) === a - (n * floor_div(a, n))
• Problem: div is not floored division!
• So how to write floor_div(a, b)?

Solution:

a | n | rem(a, n) | div(a,n) + ?
+ | + |  0        |  0
+ | + | !0        |  0
- | + |  0        |  0
- | + | !0        | -1
+ | - |  0        |  0
+ | - | !0        | -1
- | - |  0        |  0
- | - | !0        |  0
0 | + |  0        |  0
0 | + | !0        |  0
0 | - |  0        |  0
0 | - | !0        |  0
floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n) - 1, 2)
mod(a, n) === a - (n * floor_div(a, n))
• Problem: sign is not guard-safe

Solution:

floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n) - 1, 2)
sign(x) === div(x, abs(x))

mod(a, n) === a - (n * floor_div(a, n))
• Problem: sign is not guard-safe

Solution:

floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n) - 1, 2)
sign(x) === div(x, max(abs(x), 1))
mod(a, n) === a - (n * floor_div(a, n))

Solution:

floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n) - 1, 2)
sign(x) === div(x, max(abs(x), 1))
• Problem: max is not guard-safe!
max(a, b) === div(a+b + abs(a-b), 2)
mod(a, n) === a - (n * floor_div(a, n))

Solution:

floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n) - 1, 2)
sign(x) === div(x, max(abs(x), 1))
max(a, b) === div(a+b + abs(a-b), 2)
mod(x, n) === x - (n * div(x, n) + div(div(rem(x,n) * n, div(abs(rem(x, n) * n) + 1 + abs(abs(rem(x, n) * n) - 1), 2)) - 1, 2))

Solution:

oof.

mod(a, n) === a - (n * floor_div(a, n))

Optimized Solution:

floor_div(a, n) === div(a, n) + ((rem(a, n) * n) >>> abs(a * n))

'bit twiddling'

mod(a, n) === a - (n * div(a, n) + ((rem(a, n) * n) >>> abs(a * n)))

Becoming:

Let's Benchmark

list_with_10_000_integer_pairs |> Enum.map(fn {a, b} -> mod(a, b) end)

Let's Benchmark

list_with_10_000_integer_pairs |> Enum.map(fn {a, b} -> mod(a, b) end)

Only

• ~$$1.5 \times$$ slower than non-guard version
• ~$$2.2 \times$$ slower than rem

Conclusions

• Patterns with guards can be useful
• Powerful tools exist to make these more flexible
• But if you cannot structure your code this way, don't fret
• Optimizing compilers are awesome!
• Profile, Check & Benchmark if you want to know if something really is too slow

Marten / Qqwy

My company:

Me:

Wiebe-Marten Wijnja

By qqwy

• 332