Error defining a guard - invalid syntax in defguard

Using guards like this works fine:

iex(1)> defmodule M do
...(1)>   def update_position({pos, max_pos}) when pos < max_pos, do: pos + 1                     
...(1)> end
{:module, M, …
iex(2)> M.update_position({3,4})
4
iex(3)> M.update_position({3,3})
** (FunctionClauseError) no function clause matching in M.update_position/1    
    
    The following arguments were given to M.update_position/1:
    
        # 1
        {3, 3}
    
    iex:2: M.update_position/1

But when extracting it into a defguard, the compiler gives a syntax error:

iex(1)> defmodule M2 do                                                                       
...(1)>   defguard is_valid_position({pos, max_pos}) when pos < max_pos                         
...(1)> end
** (ArgumentError) invalid syntax in defguard is_valid_position({pos, max_pos})
    (elixir) lib/kernel.ex:4623: anonymous fn/2 in Kernel.validate_variable_only_args!/2
    (elixir) lib/enum.ex:737: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir) lib/enum.ex:737: Enum.each/2
    (elixir) lib/kernel.ex:4592: Kernel.define_guard/3
    (elixir) expanding macro: Kernel.defguard/1
    iex:2: M2 (module)

Anyone has a clue of the problem here?

You can not pattern match in a defguard.

1 Like

As @NobbZ mentioned, the problem is that there’s no pattern matching inside a defguard header.
I didn’t find good documentation on this, though. Only some discussion prior to implementing defguard in v1.6:

defguard(h) is extremely limited in scope:

  • the function head cannot pattern match
  • code blocks are not allowed in guards
  • no assignment
  • no local functions

[1] https://github.com/elixir-lang/elixir/issues/2469
[2] Provide defguard · Issue #2469 · elixir-lang/elixir · GitHub
[3] Redirecting to Google Groups
[4] Defguard - experimenting with a structural matching alternative to Kernel.defguard

Follow up question: if I define a guard as a macro (according to the docs) then I can pattern match. However isn’t defguard a syntactic sugar for this macro?

Using a macro you can only pattern match on the AST, not on arbitrary runtime values.

1 Like

But I was able to implement the original idea with a macro:

iex(1)> defmodule M2 do                                                 
...(1)>   defmacro is_valid_position({pos, max_pos}) do                   
...(1)>     quote do                                     
...(1)>       unquote(pos) < unquote(max_pos)              
...(1)>     end
...(1)>   end
...(1)> end
{:module, M2, …
iex(2)> defmodule M do                                                                         
...(2)>   import M2
...(2)>   def update_position({pos, max_pos}) when is_valid_position({pos, max_pos}), do: pos + 1
...(2)> end
{:module, M, …
iex(3)> M.update_position({3,4})
4
iex(4)> M.update_position({3,3})
** (FunctionClauseError) no function clause matching in M.update_position/1    
    
    The following arguments were given to M.update_position/1:
    
        # 1
        {3, 3}
    
    iex:6: M.update_position/1

This is because I’m pattern matching on the AST, not runtime values? Like: I’m pattern matching “structures” (like { }) of the language and types, not runtime values?

If it works on this macro, shouldn’t it work in the defguard expansion? Isn’t it just a macro as well?

You are matching on the AST of {pos, max_pos}, it would not work if you defined M.update_position/1 like this:

def update_position(p) when is_valid_position(p), do: elem(p, 0)

Because p has a different AST than the tuple which you specified literally before.

To be honest, the best way were probably to make this clear as “Klosbrühe” and defguard is_valid_position(pos, max_pos) when pos < max_pos or defmacro is_valid_position(pos, max_pos) do quote(do: unquote(pos) < unquote(max_pos)) end (maybe repair syntax of them).

@ijverig: You can solve this by:

defmodule M do
  defguard is_valid_position(tuple) when tuple_size(tuple) == 2 and elem(tuple, 0) < elem(tuple, 1)
end
3 Likes

Yeah that would work, but I prefer the 2 arity version as it makes no assumption about how pos and max_pos are related together.

In any case I’d add is_integer/1, is_float/1 or is_number/1 to the guard, it will safe a lot of time spent on debugging when the first time something not numeric slips into the tuple…

3 Likes

@NobbZ
Yes, I understand.
This was just an example, my need is more something like is_valid_position({x, y}, {max_x, max_y}) so I could call is_valid_position(pos, max_pos). IMHO changing that to is_valid_position(x, y, max_x, max_y) is a bit cumbersome on the readability.

But seems to be the way to go :slight_smile:

@ijverig while I haven’t used it you may like the library Expat by @vic

Expat - composable, reusable pattern matching

1 Like

Yeah this is a fantastic set, it essentially absorbed my defguard experiment (that would have worked as OP wanted if it was part of Elixir) but in a way that is stable. :slight_smile:

1 Like