Passing computed list to an Elixir macro

macros

#1

I have a map that I want to use a single source of truth for a couple of functions. Let’s say it is:

source_of_truth = %{a: 10, b: 20}

I’d like the keys of that map to be values of EctoEnum. EctoEnum provides a macro defenum that I should use like this:

  defenum(
    EnumModule,
    :enum_name,
    [:a, :b]
  )

I don’t want to repeat [:a, :b] part. I’d like to use the keys from the map instead like this:

  defenum(
    EnumModule,
    :enum_name,
    Map.keys(source_of_truth)
  )

It doesn’t work because defenum macro expects a plain list.

I thought I could do it by defining my own macro like this:

 defmacro dynamic_enum(enum_module, enum_name, enum_values) do
   quote do
     defenum(
       unquote(enum_module),
       unquote(enum_name),
       unquote(enum_values)
     )
   end
 end

and then call:

dynamic_enum(EnumModule, :enum_name, Map.keys(source_of_truth))

However, it doest the same thing: enum_values is not a precomputed list but AST for Map.get. My next approach was:

 defmacro dynamic_enum(enum_module, enum_name, enum_values) do
   quote do
     values = unquote(enum_values)
     defenum(
       unquote(enum_module),
       unquote(enum_name),
       ?
     )
   end
 end

Not sure what I could put where the ? is. I can’t just put values because it is a variable and not a list. I can’t put unquote(values) either.

A solution that sort of works is this one:

defmacro dynamic_enum(enum_module, enum_name, enum_values) do
  {values, _} = Code.eval_quoted(enum_values)
  quote do
    defenum(
      unquote(enum_module),
      unquote(enum_name),
      unquote(values)
    )
  end
end

However, the docs say that using eval_quoted inside a macro is a bad practice.

What is a good solution for that problem?

Also posted on SO: https://stackoverflow.com/questions/53651520/passing-computed-list-to-an-elixir-macro


#3

Does not this work?

  defenum(
    EnumModule,
    :enum_name,
    unquote(Map.keys(source_of_truth))
  )

EDIT: Although that depends on in what scope it is run in, if top level then no, eval’ing it there would be best.


#4

Unfortunately that doesn’t work and that is the main issue:

** (Protocol.UndefinedError) protocol Enumerable not implemented for {:unquote, ...
Since defenum is a macro, it passes its last argument as an AST and the definition expects a literal list. That is why I thought it might be possible to write a macro that first evaluates the third argument and then passes a literal list to defenum.


#5

Yeah so need to wrap it up in an eval then at the scope it is being run in.


#6

I got an excellent answer on SO

Thanks guys for your help!