sainteven
sainteven

Reputation: 37

Expand range in macro

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

Answers (1)

Dogbert
Dogbert

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

Related Questions