Reputation: 37
I was wondering if it's somehow possible to expand ranges in macro's.
Currently when I try this code:
defmodule Main do
defmacro is_atom_literal(char) do
quote do:
Enum.any?(unquote([?a..?z, ?A..?Z, ?0..?9, [?_, ?-]]), &(unquote(char) in &1))
end
def test do
c = 'b'
case c do
c when is_atom_literal(c) ->
:ok
end
end
end
Main.test
I get the error "** (CompileError) test.ex: invalid quoted expression: 97..122"
. Is it possible to make this idea work?
Upvotes: 0
Views: 153
Reputation: 222040
To fix "invalid quoted expression" you can use Macro.escape/1
like this:
Enum.any?(unquote(Macro.escape([?a..?z, ?A..?Z, ?0..?9, [?_, ?-]])), &(unquote(char) in &1))
but then this throws another error:
** (CompileError) a.exs:10: invalid expression in guard
expanding macro: Main.is_atom_literal/1
a.exs:10: Main.test/0
This is because you're trying to call Enum.any?/2
in a guard, which is not allowed.
Fortunately, there is a workaround this: just join all the expressions with or
. This can be done using Enum.reduce/3
:
defmacro is_atom_literal(char) do
list = [?a..?z, ?A..?Z, ?0..?9, [?_, ?-]]
Enum.reduce list, quote(do: false), fn enum, acc ->
quote do: unquote(acc) or unquote(char) in unquote(Macro.escape(enum))
end
end
What this code does is convert is_atom_literal(c)
into:
false or c in %{__struct__: Range, first: 97, last: 122} or c in %{__struct__: Range, first: 65, last: 90} or c in %{__struct__: Range, first: 48, last: 57} or c in '_-'
which is a valid guard expression as Elixir later desugars in
for ranges and lists into even simpler statements (something like c >= 97 and c <= 122 or c >= 65 and c <= 90 or ...
).
The code still fails since your input is 'b'
while the macro expects one character. Changing 'b'
to ?b
works:
defmodule Main do
defmacro is_atom_literal(char) do
list = [?a..?z, ?A..?Z, ?0..?9, [?_, ?-]]
Enum.reduce list, quote(do: false), fn enum, acc ->
quote do: unquote(acc) or unquote(char) in unquote(Macro.escape(enum))
end
end
def test do
c = ?b
case c do
c when is_atom_literal(c) ->
:ok
end
end
end
IO.inspect Main.test
Output:
:ok
Upvotes: 1