A possibility for extending the usability guard clauses

In Guard clauses (the when... part after the destructuring section of a pattern match), we’re only allowed to use a very small subset of functions because these are the only ones that the BEAM can optimize and that are known to be pure (side-effect free).

One of the problems of this property of the BEAM, is that it becomes impossible to ‘nest’ guard clauses. In Elixir, we can write macros, allowing us to write short snippets in a guard clause that at compile-time expand to longer pieces of guard-safe functions. But great care needs to be taken to ensure that this macro does not include code that itself tries to pattern match or run guard clauses (or any other type of conditional branching for that matter) because the compiler does not allow this.

In Elixir, we have structs, which are a great type of abstraction to allow many different kinds of user-defined types and work with protocols on them. However, we cannot check properties of a struct inside a guard clause, because it is impossible to destructure a map inside a guard clause (there are no guard-safe functions that can do this).

However, it is possible to bind names to fields of your struct inside the destructuring part of the pattern match, and refer to these names inside the guard clause. But of course, this would mean hard-coding the pattern match every time I want to check if some property of a struct is true.

I think that it is possible to build an abstraction layer that allows this, though. After all, aren’t:

def foo(tuple) when elem(tuple, 0) == :ok do
  # ...
end

and

def foo({42, _} = tuple) do
  # ...
end

the same?

Therefore, why not write something like this:

  def foo(%Box{}) when is_filled(box) do
    # ...
  end

and enhance the Elixir compiler so it will cleverly rewrite it to:

  def foo(%Box{value: tmp0}) when tmp0 != nil do
    # ...
  end

?

I think that by doing this, we would be able to create a lot more possibilities for creating guard-safe macros. I believe that things like the arithmetic operators might finally be able to be made guard-safe as they could dispatch on structs then.

The only downside I can think of, is that it will make compilation a little more difficult.

What do you think? Interesting? Awesome? Horrible?

1 Like

It’d not be ‘that’ hard to do all things considered. The elixir parser would just need to be made aware of matching contexts and call the macros over the whole context instead of just the part it is defined in, I did an experiment with this with my (highly incomplete, do not have time to work on it currently, busy at work) defguard experimentation:

# Defining
defmodule StructEx do
  import Defguard
  defguard is_struct(%{__struct__: struct_name}) when is_atom(struct_name)
  defguard is_struct(%{__struct__: struct_name}, substruct_name) when is_atom(struct_name) and struct_name === substruct_name
  defguard is_exception(%{__struct__: struct_name, __exception__: true}) when is_atom(struct_name)
end


# Using
defmodule Testering do
  use Defguard
  import StructEx

  def blah(any_struct) when is_struct(any_struct), do: 2
  def blah(_), do: 0

  def blorp(exc) when is_exception(exc), do: "exceptioned"
  def blorp(val), do: "No-exception:  #{inspect val}"
end


# Testing
assert Testering.blah(%{__struct__: Blorp}) === 2
assert Testering.blah(42) === 0
assert Testering.blorp(%ArithmeticError{}) === "exceptioned"
assert Testering.blorp(%{__struct__: Blah}) === "No-exception:  %{__struct__: Blah}"

My style here is of course a huge hack, it could be done properly in the compiler itself and more cleanly.

EDIT: Your box example (I think) could be done in my library as-is though (I’ve barely implemented it, just enough for the above examples to work, but this might work):

defmodule Box do
  import Defguard
  defguard is_filled(%Box{value: v}) when v != nil
end

...
  def foo(box) when is_filled(box) do
  end
2 Likes