Guards, Guards!
Guards in Elixir (and Erlang)
Marten / Qqwy
Guards, Guards!
Guards in Elixir (and Erlang)
Marten / Qqwy
Why talk about guards?
Why talk about guards?
 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
 What are guards, exactly?
 Case, Cond, If, ... when use what?
 Custom guards & guardaware macros
 How to go overboard with writing guardsafe code
Conclusion
Summary
 What are guards, exactly?
 Case, Cond, If, ... when use what?
 Custom guards & guardaware macros
 How to go overboard with writing guardsafe code
Conclusion
4 'levels of difficulty'
1. What are guards, exactly?
1. What are guards, exactly?
What is PatternMatching, exactly?
\(\rightarrow\) Extension to patternmatching
1. What are guards, exactly?
What is PatternMatching, exactly?
\(\rightarrow\) Extension to patternmatching
\(\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
"Powerfulness"
of programming abstractions
"Powerfulness"
of programming abstractions
How powerful is patternmatching?
?! Not possible with just a pattern
How powerful is patternmatching?
+ 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
'Enhanced pattern matching'
Guards are allowed (essentially) anywhere patterns are allowed
 function clauses
 case expressions
 anonymous functions
 for and with
 try
 receive
 match?
 not allowed in `=`!
Only some functions are allowed in a guard

comparison operators(==, !=, <, >, <=, >=, ===, !==)

some arithmetic operations (+, , *, /, div, rem, abs, round, ...)

is_* "typecheck" 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_* "typecheck" 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!
Only some functions are allowed in a guard
... why?
Why not allow any userwritten code?
Problems with allowing arbitrary code:
 What if it's slow?
 What if it writes to disk, calls a server, ... ?
 What about exceptions?
 Patternmatching inside a patternmatch?
Problems with allowing arbitrary code:
 What if it's slow?
 What if it writes to disk, calls a server, ... ?
 What about exceptions?
 Patternmatching inside a patternmatch?
Solution: Restrict to a tiny subset of BuiltIn 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
...But I want to use something else in my guard...
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
the Erlang Rationale
https://rvirding.blogspot.com/2019/01/theerlangrationale.html
2. case, cond, if,... when to choose what?
`case`
 builtin 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
 Readable + Efficient
 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
 hardtoread nested ladders when you have multiple alternatives
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
 hardtoread nested ladders when you have multiple alternatives
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 nontrue
 Erlang's `if` only allows guard clauses
When to choose what?
When to choose what?
We've talked about readability + flexibility
What about performance?
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?
Answer: All of them!
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:
3. Custom Guards
& Guardaware Macros
`defguard`
 macro that expands to more complex guard at compiletime
 Also does compiletime sanitychecks!
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) nonguard 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
There are tools help to reduce friction
of working with custom datatypes
 Not perfect, but hey, it's something :)
Takeaway:
4. My journey to define
a guardsafe `modulo`
Down the rabbit hole...
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 builtin in Erlang!
mod(dividend, divisor)
Integer.mod/2 (not guardsafe)
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 guardsafe
Solution:
floor_div(a, n) === div(a, n)
+ div(sign(rem(a, n) * n)  1, 2)
sign(x) === div(x, abs(x))
What about dividing by 0?
mod(a, n) === a  (n * floor_div(a, n))
 Problem: `sign` is not guardsafe
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 guardsafe!
max(a, b) === div(a+b + abs(ab), 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(ab), 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 nonguard 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:
WiebeMarten Wijnja
Thank you!
Guards, Guards!
By qqwy
Guards, Guards!
 332