这是我正在尝试做的事情:
defmodule ArbitraryContext do
use Cake
def make_cake do
cake do
name "Chocolate"
topping do
name "Butter cream"
sweetener "Agave"
end
end
end
end
我希望ArbitraryContext.make_cake/0
沿着以下行生成嵌套结构:
%Cake{
name: "Chocolate",
topping: %Topping{
name: "Butter cream",
sweetener: "Agave"
}
}
我已经阅读了Metaprogramming Elixir和其他一些资源,但我似乎无法重现我在Elixir的Ruby中习惯的一些DSL灵活性。这似乎是错误的,因为Elixir似乎从根本上更灵活。
我一直在跟踪Metaprogramming Elixir中的“HTML DSL”示例。 HTML示例基本上更简单,因为只有一个模块正在运行 - 一个标记 - 所以它的上下文在整个嵌套过程中可以保持不变。在我的情况下,可能有几十个上下文。我将Cake宏注入上下文,该上下文成功生成了带有名称的%Cake{...}
,但是当Topping的块未加引号生成%Topping{...}
时,上下文仍为Cake
。无论我做什么,我似乎都找不到在新环境中运行该块的干净方法。
defmodule Cake do
defstruct name: nil
defmacro __using__(_) do
quote do
import Cake
end
end
defmacro cake(do: block) do
quote do
# agent-y stuff to maintain state while building the cake. not
# super important at this time
{:ok, var!(pid, Cake)} = %Cake{} |> start_state
# here's where the cake is no longer a lie and the name is set
unquote(block)
out = get_state(var!(pid, Cake))
:ok = stop_state(var!(pid, Cake))
out
end
end
defmacro topping(block) do
quote do
# oh no! block gets evaluated here. even if I double quote
# block, it still ultimately gets the Cake scope even though I'm
# passing it into Topping, which is very similar to Cake... meant
# to build up a Topping struct.
#
# I want to:
# 1) get block into Topping.topping without unquoting it
# 2) have the block unquoted in Topping's context, once in there
Topping.topping(unquote(block))
end
end
end
在Ruby中我会用Topping.class_eval
之类的东西处理这个问题......你最终会从name
获得sweetener
和Topping
,而在另一方面你最终得到一个新的Topping类实例。
我可以通过构建预先嵌套而没有DSL和所有宏来解决这个问题,可以说更清晰,但我想了解如何使用Elixir宏获得预期的结果。
我希望我能够很好地传达这个问题!
答案 0 :(得分:1)
我相信你正试图用铁路机车征服大海。虽然它仍然可以实现你想要的东西,但从elixirish的角度看它是完全错误的,无论它意味着什么。
首先,没有“背景”的概念。你拥有的一切都只是简单的旧功能。有两种情况,如果你坚持使用“上下文”一词:编译和运行时。
Elixir宏更像是C / C ++宏,但是用与主代码相同的语言编写,这可能会让你感到困惑。它们在编译阶段正在执行。
宏返回普通AST,即按原样嵌入到位。
那就是说,当你声明一个宏时:
defmacro cake(do: block), do: block
您最终拥有一个包含所有宏内联的梁(已编译的代码)。无论如何宣布他们。而已。您仍然可以使用宏来生成结构,当然,宏仍然只是简单的AST:
iex> quote do: %{name: "cake", topping: %{name: "blah"}}
{:%{}, [], [name: "cake", topping: {:%{}, [], [name: "blah"]}]}
很快,当您的宏返回struct
的引用代表时,例如正是quote do
将为它显示的内容,它将起作用。 E.g。
iex> defmodule A do
...> defmacro cake(toppling),
...> do: {:%{}, [], [name: "cake", topping: {:%{}, [], [name: toppling]}]}
...> def check, do: IO.inspect A.cake("CREAM")
...> end
{:module, A,
<<70, 79, 82, ...>>, {:check, 0}}
iex> A.check
%{name: "cake", topping: %{name: "CREAM"}}
您可能会使用此技术来实现您想要的功能,但由于生成的整个结构体在将来无法修改,因此没有多大意义。条款是不可改变的,记住它。
希望它澄清事情。如果您仍然好奇,请随意提出更多问题。
答案 1 :(得分:0)
感谢来自@dogbert和@mudasobwa的提示,我得到了这份工作。正如预期的那样,它很粗糙而且很混乱,但它确实有效:
基本DSL:
defmodule ArbitraryContext do
def make_cake do
use Cake
cake do
name "Chocolate"
topping do
name "Butter cream"
sweetener "Agave"
end
end
end
end
蛋糕:
defmodule Cake do
require Topping
defstruct name: nil, topping: nil
defmacro __using__(_) do
quote do
import Cake
end
end
defmacro cake(do: block) do
quote do
{:ok, var!(pid, Cake)} = %Cake{} |> start_state
unquote(block)
out = get_state(var!(pid, Cake))
:ok = stop_state(var!(pid, Cake))
out
end
end
defmacro topping(do: block) do
topping = Macro.escape(
Topping.topping(do: block)
)
quote do
put_state(var!(pid, Cake), :topping, unquote(topping))
end
end
defmacro name(val) do
quote do
put_state(var!(pid, Cake), :name, unquote(val))
end
end
def start_state(state), do: Agent.start_link(fn -> state end)
def stop_state(pid), do: Agent.stop(pid)
def put_state(pid, key, content), do: Agent.update(pid, fn state -> Map.put(state, key, content) end)
def get_state(pid), do: Agent.get(pid, &(&1))
end
摘心:
defmodule Topping do
defstruct name: nil, sweetener: nil
def topping(do: block) do
{:ok, pid} = %Topping{} |> start_state
Topping.run(pid, block)
out = get_state(pid)
:ok = stop_state(pid)
out
end
def run(pid, {_block, _context, ast}) do
Macro.postwalk(ast, fn segment ->
run_call(pid, segment)
end)
end
def run(pid, ast), do: ast
def run_call(pid, {method, _context, args}) do
apply(Topping, method, [pid] ++ args)
end
def run_call(pid, ast), do: ast
def name(pid, val) do
put_state(pid, :name, val)
end
def sweetener(pid, val) do
put_state(pid, :sweetener, val)
end
def start_state(state), do: Agent.start_link(fn -> state end)
def stop_state(pid), do: Agent.stop(pid)
def put_state(pid, key, content), do: Agent.update(pid, fn state -> Map.put(state, key, content) end)
def get_state(pid), do: Agent.get(pid, &(&1))
end
最后:
iex(1)> ArbitraryContext.make_cake
%Cake{name: "Chocolate",
topping: %Topping{name: "Butter cream", sweetener: "Agave"}}
尽管我喜欢DSL,但我并不认为我最终会使用这种方法。
我还尝试了一种稍微更明智的方法是放弃代理业务,直接解析AST无状态。最后,复杂性并不值得。