Expand certain macro invocation - macros

I've found similar question, but there is no explanation about expansion of certain macro invocations. For example, how do I see what this invocation expands to?
defmodule Test do
use ExUnit.Case
test "always pass", do: assert true
end

You can pretty much follow the approach from the question you linked:
defmodule Test do
use ExUnit.Case
# (other tests here)
ast = quote do
test "always pass", do: assert true
end
expanded = Macro.expand(ast, __ENV__)
IO.puts "Original:"
# IO.puts inspect(ast, pretty: true)
IO.puts ast |> Macro.to_string
IO.puts ""
IO.puts "Expanded:"
# IO.puts inspect(expanded, pretty: true)
IO.puts expanded |> Macro.to_string
end
If you run the tests now with mix test, you'll see the expanded version of the test macro:
Original:
test("always pass") do
assert(true)
end
Expanded:
(
var = {:_, [], ExUnit.Case}
contents = {:__block__, [], [{:assert, [context: CrawlieTest, import: ExUnit.Assertions], [true]}, :ok]}
message = "always pass"
(
name = ExUnit.Case.register_test(__ENV__, :test, message, [])
def(unquote(name)(unquote(var))) do
unquote(contents)
end
)
)
You can uncomment the IO.puts inspect(_, pretty: true) lines to see how the original and the expanded version look in the Abstract Syntax Tree representation.

Related

Passing computed list to an Elixir macro

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.
[EDIT]
A solution with Macro.expand does not work either because it doesn't actually evaluate anything. The expansion stops at:
Expanded: {{:., [],
[
{:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
:get_attribute
]}, [],
[
{:__MODULE__, [counter: -576460752303357631], Kernel},
:keys,
[
{:{}, [],
[
TestModule,
:__MODULE__,
0,
[
file: '...',
line: 16
]
]}
]
]}
So it does not expand to the list as we expected.
[\EDIT]
What is a good solution for that problem?
As stated in the documentation for Macro.expand/2
The following contents are expanded:
Macros (local or remote)
Aliases are expanded (if possible) and return atoms
Compilation environment macros (__CALLER__/0, __DIR__/0, __ENV__/0 and __MODULE__/0)
Module attributes reader (#foo)
Emphasis is mine. So the possibility would be to use module attributes with Macro.expand/2.
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
And call it like:
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
FWIW, the full code:
$ \cat lib/eenum.ex
defmodule Eenum do
import EctoEnum
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
end
$ \cat lib/tester.ex
defmodule Tester do
import Eenum
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
end
FWIW 2. To be able to call dynamic_enum as shown above from the module scope, all you need is (surprise :) another module scope, already compiled at the moment of macro invocation:
defmodule Defs do
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
defmacro keys, do: Macro.expand(#keys, __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
FWIW 3. The latter (explicit module with definitions) will work even without a necessity to have module attributes:
defmodule Defs do
defmacro keys, do: Macro.expand(Map.keys(%{a: 10, b: 20}), __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
The rule of thumb is when you find yourself in a need to invoke Code.eval_quoted/3, put this code into the independent module and make compiler invoke this code compilation for you. For functions is works on the module level, for module level it should be put into another module to make module context (aka __CALLER__ and __ENV__) available.
I battled with the same problem a while back. Basically you can build your syntax tree in a quote, using unquote to inject your dynamic value, and then use Code.eval_quoted to eval the macros:
options = Map.keys(source_of_truth)
Code.eval_quoted(
quote do
EctoEnum.defenum(MyEnum, :type_name, unquote(options))
end,
[],
__ENV__
)

Why the dynamic call results in “undefined function”?

I have a module that dynamically routes external calls to it’s own functions in the following way:
defmodule A do
defmacro call(name) do
quote do
fun = & unquote(:"A.#{name}")(&1)
fun.(:foo)
end
end
def test(param), do: IO.inspect(param, label: "test")
end
#⇒ {:module, A, ..., {:test, 1}}
The module was successfully compiled and A.test/1 is there.
A.test :foo
#⇒ test: :foo
Now I try to call it as:
defmodule B do
require A
def test, do: A.call(:test)
end
#⇒ ** (CompileError) iex:21: undefined function A.test/1
# (stdlib) lists.erl:1338: :lists.foreach/2
# (stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
# (iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
What is wrong with this dynamic call dispatch and why the error message contradicts the reality?
The error message is misleading. & unquote(:"A.#{name}")(&1) will call a function literally named A.test in the current scope, not the test/1 function of module A:
defmodule A do
defmacro call(name) do
quote do
fun = & unquote(:"A.#{name}")(&1)
fun.(:foo)
end
end
def unquote(:"A.test")(param), do: IO.inspect(param, label: "!!!")
end
defmodule B do
require A
import A
def test, do: A.call(:test)
end
B.test
Output:
!!!: :foo
To make it call the test/1 function of module A, you can do & A.unquote(:"#{name}")(&1):
defmodule A do
defmacro call(name) do
quote do
fun = & A.unquote(:"#{name}")(&1)
fun.(:foo)
end
end
def test(param), do: IO.inspect(param, label: "test")
end
defmodule B do
require A
def test, do: A.call(:test)
end
B.test
Output:
test: :foo

How to dynamically generate a Phoenix controller function with a map argument?

So I'm using Phoenix 1.3, and I created a macro to generate a function and inject it into a controller.
Based on the number I pass in, I want it to generate a map with that many parameters that are named "id1", "id2", etc. all the way up to "id#{number}". This map will be part of the argument list along with the usual Phoenix "conn".
So I want to generate a method like this to be pattern matched against and "some stuff" can be executed:
def index(conn, %{"id1" => id1, "id2" => id2}) do
# some stuff
end
when I call the macro create_some_function_by_number("index", 2).
My macro looks something like:
defmacro create_some_function_by_number(name, num) do
params =
for n <- 1..num, do: %{"id#{n}" => Macro.var(:"id#{n}", nil)}
|> Map.new
quote do
def unquote(:"#{name}")(unquote(Macro.escape(params)) do
# some code here for the index action
end
end
end
1) How do I inject the "conn" into the function head so it can be pattern matched against?
2) Is this the correct way to create the map to be pattern matched against?
While you can definitely use macros in this way, you probably should not. Here is a working solution with comments:
defmodule MyMacro do
defmacro create_some_function_by_number(name, num, do: block) do
params =
for n <- 1..num do
{"id#{n}", Macro.var(:"id#{n}", nil)}
end
# We can't call Macro.escape because it is for escaping values.
# In this case, we have a mixture of values "id#{n}" and
# expressions "Macro.var(...)", so we build the map AST by hand.
pattern =
{:%{}, [], params}
conn =
Macro.var(:conn, nil)
quote do
def unquote(:"#{name}")(unquote(conn), unquote(pattern)) do
unquote(block)
end
end
end
end
defmodule MyExample do
import MyMacro
create_some_function_by_number :index, 2 do
{conn, id1 + id2}
end
end
IO.inspect MyExample.index(:conn, %{"id1" => 1, "id2" => 2})
As you can see, macros can make the code harder to understand. If you can solve it at runtime, it should definitely be preferred.

Why is the error on the wrong line?

I am diving into Metaprogramming Elixir by Chris McCord.
I made a spelling mistake while typing one of examples:
defmodule Math do
defmacro say({:+, _, [lhs, rhs]}) do
qoute do #spelling mistake (swapped "o" and "u")
lhs = unquote(lhs) #(CompileError) math.exs:4: undefined function lhs/0
rhs = unquote(rhs)
result = lhs + rhs
IO.puts "#{lhs} plus #{rhs} is #{result}"
result
end
end
defmacro say({:*, _, [lhs, rhs]}) do
qoute do #spelling mistake copied
lhs = unquote(lhs)
rhs = unquote(rhs)
result = lhs * rhs
IO.puts "#{lhs} times #{rhs} is #{result}"
result
end
end
end
In the shell, the errors are meaningful:
iex(1)> qoute do: 1 + 2 #spelling mistake
** (RuntimeError) undefined function: qoute/1
iex(1)> unquote do: 1
** (CompileError) iex:1: unquote called outside quote
Why compiling this file gives error in the next line? Is my spelling mistake some valid construct?
The error appears in correct file, if I remove the unquote.
defmodule Math do
defmacro say({:+, _, [lhs, rhs]}) do
qoute do #function qoute/1 undefined
result = lhs + rhs
IO.puts "#{lhs} plus #{rhs} is #{result}"
result
end
end
...
Why using unquote moved the error somewhere else?
That's because once you call qoute/1, Elixir assumes it is a function that will be defined later, and proceeds to compile the code as a function call. However, when we try to compile it, we see an unquote, assume there is a variable defined outside, and everything crashes when it doesn't.
There is no way we can work around it because the error happens when we are expanding code and is exactly when quote/unquote are expanded too.

How can I refer to a module variable in a function without referring to its module in Elixir 1.0.3? In its parent scope?

I'd like to make a function in Elixir 1.0.3 refer to a variable inside its "parent" scope. In this case, its parent scope is a module.
Here's the same code as that which I used in my last question:
defmodule Rec do
def msgurr(text, n) when n <= 1 do
IO.puts text
end
def msgurr(text, n) do
IO.puts text
msgurr(text, n - 1)
end
end
If I change it to the following:
defmodule Rec do
counter = "done!"
def msgurr(text, n) when n <= 1 do
IO.puts text
IO.puts Rec.counter
end
def msgurr(text, n) do
IO.puts text
msgurr(text, n - 1)
end
end
It compiles just fine, but I get the following error if I try the msgurr function:
** (UndefinedFunctionError) undefined function: Rec.counter/0
Rec.counter()
recursion_and_import_test.exs:5: Rec.msgurr/2
I also tried the following:
defmodule Rec do
counter = "done!"
def msgurr(text, n) when n <= 1 do
import Rec
IO.puts text
IO.puts Rec.counter
end
def msgurr(text, n) do
IO.puts text
msgurr(text, n - 1)
end
end
I get a compile-time warnings here, though:
➜ ubuntu elixirc recursiontest.exs
recursion_and_import_test.exs:1: warning: redefining module Rec
recursion_and_import_test.exs:2: warning: variable counter is unused
recursion_and_import_test.exs:4: warning: unused import Rec
When I attempt to use the msgurr function:
➜ ubuntu iex
Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:8:8] [async-threads:10] [kernel-poll:false]
Interactive Elixir (1.0.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> import Rec
nil
iex(2)> Rec.msgurr("blah", 3)
blah
blah
blah
** (UndefinedFunctionError) undefined function: Rec.counter/0
Rec.counter()
recursiontest.exs:6: Rec.msgurr/2
I seem to be unable to import a variable of my own from a module into a function inside that module.
I've gone over the import documentation, but I can't seem to make much sense from it of how to do this sort of thing. Should I check the Erlang docs?
You are confusing Modules with Objects.
Rec.counter
Always refers to the function inside the Rec Module. That's what the error messages are telling you, they can't find the function. Modules can't have variables in the way you are thinking of them.
Modules can have attributes. While it might be possible to fudge want you want
with a module attribute, you should just make a function that returns a constant if you want to reference it using Rec.counter.
def counter do
"done!"
end
There's more on module attributes here, but if you want to be able to think in elixir, you need to start thinking "functions not variables".