__using__ macro with 3-tuple arguments

I’m looking at some Macro code that generated functions using __using__ – and it had resorted to Code.eval_quoted to handle the keyword list parameters.

It looks like this was done because the code blows up when handling arguments of tuple size 3 without it (which does look AST-like).

Is it required to do that?

Here’s a snippet demonstrating the problem:

defmodule Options do
  defmacro __using__(opts) do

    IO.inspect(Keyword.get(opts, :cat), label: "compile time value")

    quote do
      def show() do
        IO.inspect(unquote(opts))
      end
    end
  end
end

defmodule Options.Normal do
  use Options, cat: "meow"
end

defmodule Options.Quoted do
  use Options, cat: [{"this", "ok"}]
end

defmodule Options.Broken do
  use Options, cat:  [{"¿", "?", :spanish_query}]
end

and the compiled output:

Compiling 1 file (.ex)
compile time value: "meow"
compile time value: [{"this", "ok"}]
compile time value: [{:{}, [line: 23], ["¿", "?", :spanish_query]}]

Note, the three tuple gets converted into AST syntax which subverts my expectations…

Besides avoiding macros, is there a best practice way of handling this?

Thanks!

Your expectations are just a little off: all inputs to macros are quoted AST.

It just so happens that the first two inputs have the same form when represented as AST: atoms, strings, lists, and two-tuples have the same form as literals and quoted values.

So your third value is not “blowing up”, it’s just giving you a deeper glimpse into Elixir’s AST, when you provide the macro a value that does not look the same when quoted.

Code.eval_quoted should not be required here: unquote-ing all three terms will behave as expected. If, debugging mid-macro, you want to see what the unquoted value of some AST would look like, use Macro.to_string:

defmodule Options do
  defmacro __using__(opts) do

    option = Keyword.get(opts, :cat)

    IO.inspect(option, label: "compile time value:")

    option
    |> Macro.to_string
    |> IO.puts

    quote do
      def show() do
        IO.inspect(unquote(opts))
      end
    end
  end
end

defmodule Options.Normal do
  use Options, cat: "meow"
end
#=> compile time value:: "meow"
#=> "meow"

defmodule Options.Quoted do
  use Options, cat: [{"this", "ok"}]
end
#=> compile time value:: [{"this", "ok"}]
#=> [{"this", "ok"}]

defmodule Options.Broken do
  use Options, cat:  [{"¿", "?", :spanish_query}]
end
#=> compile time value:: [{:{}, [line: 24], ["¿", "?", :spanish_query]}]
#=> [{"¿", "?", :spanish_query}]

Note that this literals-same-as-quoted behaviour is why you are able to successfully call Keyword.get(opts, :cat) to extract single options from the macro args! Keywords are a datastructure that can pass into macros unchanged but for their values.

This is in fact how do blocks work under the covers, getting converted to Keywords. It’s also why the one-liner do: code form works as well: you are just doing the Keyword conversion yourself instead of letting the compiler perform it.

For fun, try passing your macro a do block: you’ll be able to extract the AST of the block from your macro’s opts via Keyword.get(opts, :do).

5 Likes

Thanks for the extended reply!

It seems I failed to really convey the issues that I’ve been encountering.

I’ve been trying to come up with an example that illustrates the problem I’ve been having exactly – and failing to do so :frowning: – all of my broken code is in some way misleading, so I’m just going to try to write out what I’m attempting to fix.

Basically, I’ve got a list of 3-tuples passed in via the keyword list of options.

I want to iterate over this list and generate a def for each option such that:

cat: [{"meow", 1, 2}, {"woof", 3, 4}]

would produce

def show("meow") do
   1 + 2
end

def show("woof") do
  3 + 4
end

I can’t seem to find a way (other than Code.eval) to iterate over the list and extract the sub-values within it such that I can quote up the appropriate functions.

At best, I end up generating
def show(:{}) do

It’s just aggravated me that I can’t seem to work this out… I assume there’s some canonical way to do it that I am blindly stumbling past…

Thanks!

IMO this is the source of confusion - at macro-evaluation time, you don’t have that. You have this:

iex(1)> quote do
...(1)>   [cat: [{"meow", 1, 2}, {"woof", 3, 4}]]
...(1)> end

[cat: [{:{}, [], ["meow", 1, 2]}, {:{}, [], ["woof", 3, 4]}]]

The “canonical way” to handle this in your macro is to pattern-match on the AST structures you get.

3 Likes

OK, that makes sense!

I was reticent to actually work against the AST value (it felt “wrong” I guess?).

Thanks!

Nope, it’s normally the right thing to do! You should strive for using quote when you are composing AST, but feel comfortable using pattern-matching when decomposing it. It’s one of the really cool parts of Elixir’s homoiconicity, using the language’s strengths to parse it itself. :slight_smile:

2 Likes

Also, if the list of arguments to use is getting unwieldy consider extracting those details to DSL-style plumbing; there’s a reason why it’s use Ecto.Schema; ... schema do not use Ecto.Schema, [way: too: many :options]

1 Like

This should be interesting for you:

defmodule Example do
  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      for {first, second, third} <- opts[:cat] do
        def show(unquote(first)) do
          unquote(second) + unquote(third)
        end
      end
    end
  end
end

defmodule Sample do
  use Example, cat: [{"meow", 1, 2}, {"woof", 3, 4}]
end

for string <- ["meow", "woof"] do
  string |> Sample.show() |> IO.inspect(label: string)
end

Following documentation:

:bind_quoted - passes a binding to the macro. Whenever a binding is given, unquote/1 is automatically disabled.

Source: Kernel.SpecialForms — Elixir v1.16.0

Mentioned documentation is long and have lots of examples. However for your use case I recommend you to read this part: Binding and unquote fragments.

2 Likes

That said, there are a handful of situations where you really do require the options to be read at compile-time, and require those options to allow a wide variety of Elixir datastructures (so pattern-matching on the AST is infeasible).
Those are the situations in which using Code.eval_quoted makes sense.

1 Like

I’m a little skeptical about this and curious about the scenario you pose. I can’t think of a concrete situation where macros (evaluated at compile time) and unquoting their options (read at compile time) generate code (which then gets compiled) could not accomplish pattern matching on Elixir datastructures. I also think Code.eval_quoted is a risky escape hatch from a hygenic macro system.

I’d love to see an example of when you would use Code.eval_quoted and see if I can come up with an alternative solution, if you have the time and inclination to put such a sample together, or link to something pre-existing!

The situation arises when you want your macro to make a clever decision at compile-time about what code should be included in the final module. One obvious (but even more obscure) case is when we need to evaluate code at compile-time in a different context than where the quoted code is inserted.
But in the case the context is the same, we can indeed usually (always?) rewrite

{result, []} = Code.eval_quoted(code, __CALLER__)
if some_condition(result) do
  a = complicated_logic()
  quote do foo |> unquote(a) |> bar end
else
  b = other_complicated_logic()
  quote do baz |> unquote(b) |> qux end
end

as

quote do
  if some_condition(unquote(result)) do
    foo |> unquote(complicated_logic()) |> bar
  else
    baz |> unquote(other_complicated_logic()) |> qux
  end
end

However, note the inversion of control: If there is complex compile-time logic to be ran inside the if (as shown here), I prefer the former example over the latter.

Note that it is in general not possible to move the logic inside the quote, because

a = foo()
quote do 
... 
some_fun(unquote(a))
... 
end

is not the same as

quote do 
...
a = unquote(foo()) 
some_fun(a)
... 
end

because there are situations in which multi-line expressions are not supported. This is for instance the case in AST to generate patterns, guard clauses or type expressions.


One place I am using it (Code.eval_quoted at compile-time) is in the TypeCheck library. See here, and the various places where TypeCheck.Type.build_unescaped/3 is used.
What code happens to be added at compile-time depends on the result of build_unescaped but varies depending on the location it is used.

2 Likes