Access to variables defined in quote block

I have a set of repetitive functions for different module and would like to give macro a try but facing some problems.

This is a sample module:

defmodule MyModule do
  @props [
    %{name: "first_value", id: 0x01},
    %{name: "second_value", id: 0x02},
  ]
  @moduledoc """
    iex> MyModule.get_id_name(0x01)
    "first_value"
  """
  use MyApp
end

and this is the source of MyApp:

defmodule MyApp do
  defmacro __using__(_opts) do
    quote do
      for prop <- @props do
        def get_id_name(prop[:id]) do
          prop[:name]
        end
      end
    end
  end
end

When I compile this code, it gives me this error:

(CompileError) lib/my_app.ex:22: cannot invoke remote function Access.get/2 inside match

Which is fair! I tried to use unquote(prop[:id]) but of course it doesn’t work as well

lib/my_app.ex:5: undefined function prop/0

unquote is useful when the data is defined outside of quote

Any help is appreciated

This might help. I

  1. Moved the comprehensive outside the quote
  2. Moved the map to be an argument to using which seems cleaner
  3. Code.eval_quoted the opts because of the way maps are passed to the macro as AST
  4. Unquoted the prop since that comes from the outer context
defmodule MyApp do
  defmacro __using__(opts) do
    for prop <- opts do
      {prop, _} = Code.eval_quoted(prop)
      quote do
        def get_id_name(unquote(prop[:id])) do
          unquote(prop[:name])
        end
      end
    end
  end
end

defmodule MyModule do
  use MyApp, [
    %{name: "first_value", id: 0x01},
    %{name: "second_value", id: 0x02},
  ]
end

Thanks @kip :grin:

The problem here is I also have other stuff in quote block and looking for an option to keep it while doing the for each. so by looking at your answer I came up with

defmodule MyApp do
  defmacro __using__(opts) do
    macro_props = for prop <- opts do
      {prop, _} = Code.eval_quoted(prop)
      quote do
        def get_id_name(unquote(prop[:id])) do
          unquote(prop[:name])
        end
      end
    end
    rest = quote do
      def other_func do
        :ok
      end
    end
    [rest] ++ macro_props
  end
end

Does it make sense or there is a better way to do that?

1 Like

A better way would be, I think, to import the other functions so as to keep the macro code as concise as possible, vis:

defmodule MyApp do
  defmacro __using__(opts) do
    for prop <- opts do
      {prop, _} = Code.eval_quoted(prop)
      quote do
        def get_id_name(unquote(prop[:id])) do
          unquote(prop[:name])
        end
      end
    end
  end
  
  def other_function(thing) do
    IO.puts thing
  end
end

defmodule MyModule do
  import MyApp
  use MyApp, [
    %{name: "first_value", id: 0x01},
    %{name: "second_value", id: 0x02},
  ]
end

2 Likes

If you want to omit the import you can have the macro do that too:

defmodule MyApp do
  defmacro __using__(opts) do
    imp = quote do
      import MyApp
    end
    
    funs = for prop <- opts do
      {prop, _} = Code.eval_quoted(prop)
      quote do
        def get_id_name(unquote(prop[:id])) do
          unquote(prop[:name])
        end
      end
    end
    
    [imp | funs]
  end
  
  def other_function(thing) do
    IO.puts thing
  end
end

defmodule MyModule do
  use MyApp, [
    %{name: "first_value", id: 0x01},
    %{name: "second_value", id: 0x02},
  ]
  
  def test do
    other_function("x")
  end
end
4 Likes