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

  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

4 'levels of difficulty'

1. What are guards, exactly?

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

"Powerfulness"

of programming abstractions

"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

'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_* "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!

Only some functions are allowed in a guard

... 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, ... ?
  • What about exceptions?
  • 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, ... ?
  • What about exceptions?
  • 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

...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

2. case, cond, if,... when to choose what?

`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
  • 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
  • hard-to-read 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
  • hard-to-read 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 non-true
  • 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
& Guard-aware Macros

`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

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 guard-safe `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 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))

What about dividing by 0?

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

Thank you!

Guards, Guards!

By qqwy

Guards, Guards!

  • 332