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)
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:
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?
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).
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…
@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.
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.