I would like to define a macro such that I can pass a do block into it, and have the macro create a function that calls the block, with an argument. I am running into a chicken & egg issue because the code below complains that name is not defined.
defmodule MyMacro do
defmacro greet(do: block) do
quote do
def hello(name), do: unquote(block)
end
end
end
defmodule Test do
import MyMacro
greet do
IO.puts("Hello, #{name}!")
end
end
Attempting to compile this code results in:
(CompileError) iex:6: undefined function name/0
(stdlib) lists.erl:1338: :lists.foreach/2
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
(iex) lib/iex/evaluator.ex:250: IEx.Evaluator.handle_eval/5
From my understanding, this blows up even before it gets to my macro because elixir attempts to generate an AST for the do block before calling my macro, but name is undefined.
My goal is to be able to call Test.hello("world") after compiling the DSL. Is that possible in Elixir?
Elixir's macros are hygienic so if you declare a variable in a macro's quote, it won't be available to the caller. You can disable this by wrapping the declaration of the variable with var!:
quote do
def hello(var!(name)), do: unquote(block)
end
Related
I'm trying to get a better understanding of Elixir macros, but I'm having trouble figuring out how to set up a scenario where I can pattern match the argument that I pass to a macro when the value I'm trying to pass is a variable. Here's a simple example to illustrate:
macro_test.ex
defmodule MacroTest do
use MacroTest.UseMe
def run() do
atom = :hello_world
macro_a(atom)
end
end
use_me.ex
defmodule MacroTest.UseMe do
defmacro __using__(_) do
quote do
defmacro macro_a(:hello_world) do
quote do
"Success!"
end
end
defmacro macro_a(:some_other_atom) do
quote do
"Did something else..."
end
end
end
end
end
When I try to compile this code, I get the error
== Compilation error in file lib/macro_test.ex ==
** (FunctionClauseError) no function clause matching in MacroTest.macro_a/1
If I change the initial run() function so that the atom is passed directly to the macro, such as macro_a(:hello_world), then it compiles/runs just fine.
How can I alter this scenario so that the macro can pattern match on a value that has been provided as a variable, rather than the literal value itself?
It depends on what you want to do. Macros run at compile time, and operate on portions of source code (as opposed to the run time value returned by said piece of source code). So in this case, the macro knows that it has been called with a variable called atom, but it has no way of knowing that this variable has been assigned to earlier in the calling function.
You could check the value of the given variable at run time:
defmacro macro_a(a) do
quote do
case unquote(a) do
:hello_world ->
"Success!"
:some_other_atom ->
"Did something else..."
end
end
end
That is, every invocation of macro_a gets replaced by the case expression above, using whatever variable or other expression was passed as argument.
I'm trying to invoke a private macro, within a quote block, using a variable defined within the code block itself.
This is the pseudo-code showing what I would like to do (doesn't work)
defmodule Foo do
defmacrop debug(msg) do
quote bind_quoted: [msg: msg], do: IO.puts(msg)
end
defmacro __using__(_) do
quote do
def hello do
my = "testme"
unquote(debug(quote do: my))
end
end
end
end
defmodule Bar do
use Foo
end
Bar.hello()
And this would get converted (in my mind), at compile time to:
defmodule Bar do
def hello do
my = "testme"
IO.puts(my)
end
end
Is there any way to achieve this? I'm struggling to find any documentation related to it.
Update
I discovered that:
defmodule Foo do
defmacrop debug() do
quote do: IO.puts("hello")
end
defmacro __using__(_) do
quote do
def hello do
my = "testme"
unquote(debug())
end
end
end
end
Gets properly converted to what I need, but I'm struggling find a way to pass the variable as is, so that it becomes IO.puts(my)
The issue here is with nested quoting: the private macro should return the double-quoted expression (since to invoke it from the outer scope one needs to explicitly unquote, and macro is still expected to return a quoted expression.)
Sidenote: your update section is wrong; you might notice, that "hello" is printed during a compilation stage, namely when use Foo is being compiled. That is because the double-quoting is needed, the code in your update section executes IO.puts when unquote in __using__ macro is met.
On the other hand, my should be quoted only once. That might be achieved with an explicit quoting of AST, passing the msg there as is:
defmodule Foo do
defmacrop debug(msg) do
quote bind_quoted: [msg: msg] do
{
{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]},
[],
[msg]} # ⇐ HERE `msg` is the untouched argument
end
end
defmacro __using__(_) do
quote do
def hello do
my = "testme"
unquote(debug(quote do: my))
end
end
end
end
defmodule Bar do
use Foo
end
Bar.hello()
#⇒ "testme"
I was unable to achieve the same functionality with options in the call to Kernel.SpecialForms.quote/2; the only available related option is unquote to tune the unquoting inside nested quotes, while we need the exact opposite.
Sidenote: below does not work and I expect this to be a bug in Kernel.SpecialForms.quote/2 implementation.
quote bind_quoted: [msg: msg] do
quote bind_quoted: [msg: msg], do: IO.puts(msg)
end
FWIW: I filed an issue.
I believe it might be a good feature request to Elixir core, to allow an option that disables additional quoting.
Sidenote 2: the following works (most concise approach):
defmacrop debug(msg) do
quote bind_quoted: [msg: msg] do
quote do: IO.puts(unquote msg)
end
end
So you might avoid tackling with an explicit AST and just use the above. I am leaving the answer as is, since dealing with AST directly is also a very good option, that should be used as a sledgehammer / last resort, which does always work.
If IO.puts is not your desired target, you might call quote do: YOUR_EXPR on what you want to have in debug macro:
quote do: to_string(arg)
#⇒ {:to_string, [context: Elixir, import: Kernel], [{:arg, [], Elixir}]}
and manually unquote the arg in the result:
# ✗ ⇓⇓⇓ {:arg, [], Elixir}
# ✓ ⇓⇓⇓ arg
{:to_string, [context: Elixir, import: Kernel], [arg]}
This is basically how I got the AST of your original request (IO.puts.)
With elixir's __using__ macro and with the aid of __on_definition__ I can keep track of all methods in a module. What I would like to do is replace all methods in a module with some other implementation. Now it's fairly trivial to create the new methods I want but I also want to remove the existing methods.
Additionally to this, is there a way to apply a macro to several modules without explicitly adding the use XXX to each module. In otherwords if I have a folder structure:
foo/
bar/
module1.ex
module2.ex
Could I dynamically apply the using macro to everything in ./foo/bar/.
To simplify my problem, imagine that for all methods in all modules in the foo/bar/ folder I want to change there implementation so that they will first run IO.inspect "called before method", aka some kind of before method aop.
def is just a macro from Kernel. So you could exclude it with import Kernel, except: [def: 2] and then create you def. Here is something that almost works, except for the return value.
defmodule Def do
import Kernel, except: [def: 2]
defmacro __using__(_) do
quote do
import Kernel, except: [def: 2]
end
end
defmacro def(call, expr) do
# caller = __CALLER__
# IO.inspect call
# IO.inspect expr
# import Kernel
quote do
Kernel.def unquote(call) do
IO.puts "do something first"
unquote(expr)
end
end
end
end
defmodule Special do
use Def
import Def
def test(one, two) do
IO.puts "one: #{inspect one}, two: #{inspect two}"
end
end
Running in iex
Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Special.test 1, 2
do something first
one: 1, two: 2
[do: :ok]
iex(2)>
I left some print statement at the top of the custom def macro. You can use this to examine the AST of the inputs to the new def macro.
You can find more information in the Kernel source code.
I have not thought of an idea to automatically apply the use to multiple modules at once. I'm come back and update this if I think of anything.
This is common elixir:
defmodule Fizz do
defmacro asdf, do: IO.puts("asdf")
end
defmodule Buzz do
require Fizz
Fizz.asdf
end
However, although you can reference macros in the same context like:
defmodule Fizz do
# ...
defmacro asdf_qwer, do: asdf && IO.puts("qwer")
end
... you can't reference macros in the body of the same module that defined them:
defmodule Fizz do
defmacro asdf, do: IO.puts("asdf")
asdf
end
This raises undefined function asdf/0.
Is there a workaround for this "problem"? Sometimes I may want to use macros to remove some boilerplate from the module I'm working on, and that macro's functionality may be specific enough not to put it in another module.
The reason that we're getting undefined function errors here is because at compile time, the asdf macro does not yet exist.
So we need to notify the compiler that an extra step is required just before compilation is finished.
One option is the #after_compile module callback attribute which lets you invoke some code just after compilation in order to perform a final bit of code generation.
For example:
defmodule M do
#after_compile __MODULE__
def __after_compile__(env, _bytecode) do
IO.inspect env
end
end
Another option is to put your macros in a Fizz.Macros module.
In Erlang I can use define macro or .hrl file to keep the configuration at one place. Whats the best place to do it in Elixir.
I couldn't find any elegant way of doing it. Right Now I am doing something like:-
def get_server_name do
"TEST"
end
Am I missing something?
Whether you use functions or macros should matter too much in the end, but if what you're looking for is the "keeping it in one place" part, I'd suggest putting it in its own namespace/module
defmodule MyApp.Configuration
def server_name do
"foo"
end
# or if you prefer having it all on one line
def host_name, do: "example.com"
# for complete equivalency, you can use a macro
defmacro other_config do
"some value"
end
end
and then in your app, instead of including the file, you can alias the module so have a short prefix to indicate it's configuration, and indicate they come from elsewhere
defmodule MyApp.Server do
alias MyApp.Configuration, as: C
end
or if you want to use the names directly
defmodule MyApp.Server do
import MyApp.Configuration
end