Hello everyone!
I am new to Elixir and need help to solve this little problem.
I have two macro:
defmacro state(state_id, do: expr) do
quote do
def process_state({:next, {unquote(state_id), state_info}=var!(state)}, update) do
case update do
unquote(expr)
end
end
end
end
defmacro command(cmd_text, do: expr) do
quote do
{:message, %{text: text} , _} ->
unquote(expr)
end
end
I use ‘command’ macro in ‘state’ macro like this:
state :state_new do
command "help", do
{:ok, ""}
end
end
And have this error message:
== Compilation error on file lib/user/server.ex ==
** (CompileError) lib/user/server.ex:42: expected -> clauses for do in case
(stdlib) lists.erl:1354: :lists.mapfoldl/3
lib/user/server.ex:42: (module)
Because state gets the complete AST. If you wan’t it to get expanded, you need to do it manually by using Macro.expand/2 or Macro.expand_once/2, depending on your needs.
defmacro state(state_id, do: expr) do
quote do
def process_state({:next, {unquote(state_id), state_info}=var!(state)}, update) do
case update do
unquote(Macro.expand(expr, __CALLER__))
end
end
end
end
defmacro command(cmd_text, do: expr) do
quote do
{:message, %{text: text} , _} ->
unquote(expr)
end
end
And as i say it works for code:
state :state_new do
command "help", do
{:ok, ""}
end
end
But not for code
state :state_new do
command "help", do
{:ok, ""}
end
command "next", do
{:ok, ""}
end
end
expand can’t process expr and output original expr. What can i do to make this code works?
I’m not quite sure what you mean by this, but as in the documentation of escape/2 is written, we only expand the given node, if you only have a single item in the list your node is {:command, ...}, while when you have 2 items or even more, your node will be {:__block__, ...}, which is not an expandable macro. In this case you will have traverse the AST on your own. Be careful though. It might totally make sense to not expand bliendly!
In general, I think it would make more sense to generally handle command in state, since a command without state doesn’t make any sense. It is only valid inside a state block.
No, I mean you should not introduce a macro command, but traverse the list of items in the :__block__, and handle :command entries there in some walk-function.
You can steal the basic idea of this from my dot dsl submission on exercism:
I have to admit, I am to tired to tailor an example that actually works with your syntax. Maybe tomorrow, but I do not promise anything.
Thank You! I understand basic idea, but i need to learn more about macro & AST.
And after all I can’t understand why command not expanded automatically? Many examples over internet use macro inside macro without walking AST or use Macro.expand (but they not generate ‘case’ statement).
Because Macro.expand/2 and co do not walk the AST, they only try to expand the root-node. And since __block__ is not a macro, there is nothing to expand.
I can’t tell you much about that examples, or explain why they work, without seeing them. Perhaps you can link to them and we go through the differences?
If you want to go deeper into macros and metaprogramming, I’d suggest to read Metaprogramming Elixir by @chrismccord. I’m only through the first quarter , and it will take me probably about another year for each remaining one, but it really helped me much, even when I read only this little.
defmodule T do
defmacro ma(do: expr) do
v = Macro.var(:a, nil)
quote do
def f() do
unquote(v) = 1
unquote(expr)
end
end
end
defmacro mb(do: expr) do
v = Macro.var(:b, nil)
quote do
unquote(v) = 5
unquote(expr)
end
end
end
defmodule Example do
require T
T.ma do
T.mb do
IO.puts(a+b)
end
T.mb do
IO.puts(a+b*2)
end
end
end
This is output:
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Example.f()
6
11
:ok
iex(2)>
In my opinion it is very similar, but without case statement.
Basically this is because macros are expanded from the outer to the inner. After expanding the outer macro the inner syntax needs to stay valid. And exactly thats the issue with your code.