Guards in Elixir (and Erlang)
Marten / Qqwy
Guards in Elixir (and Erlang)
Marten / Qqwy
I'll be mostly showing Elixir code
Conclusion
Conclusion
\(\rightarrow\) Extension to pattern-matching
\(\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
How powerful is pattern-matching?
?! Not possible with just a pattern
How powerful is pattern-matching?
+ guards
"extra boolean checks"
def abs(x) when x < 0, do: -x
def abs(x) when x >= 0, do: x
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
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!
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 not allow any user-written code?
Solution: Restrict to a tiny subset of Built-In Functions! ('BIFs')
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:
Some things you cannot do:
"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/the-erlang-rationale.html
def sign(x) do
case x do
x when x == 0 -> 0
x when x > 0 -> 1
x when x < 0 -> -1
end
end
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
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)
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)
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
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
However... Elixir does not use it, because:
We've talked about readability + flexibility
What about performance?
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
Check, Profile and Benchmark if you want to be sure!
Takeaway:
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
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
Takeaway:
mod(dividend, divisor)
Integer.mod/2 (not guard-safe)
mod(a, n) === a - (n * floor_div(a, n))
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))
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))
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))
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))
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:
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:
list_with_10_000_integer_pairs |> Enum.map(fn {a, b} -> mod(a, b) end)
list_with_10_000_integer_pairs |> Enum.map(fn {a, b} -> mod(a, b) end)
Only
My company:
Me:
Wiebe-Marten Wijnja