[英]Creating a DSL using nested Elixir macros that can change block context
这是我想要做的:
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
东西来处理这个Topping.class_eval
……你最终会得到来自Topping
name
和sweetener
,而另一方面你会得到一个新的 Topping 类实例。
我可以解决这个问题,可以说更简洁,只需构建没有 DSL 和所有宏的预嵌套结构,但我想了解如何使用 Elixir 宏获得预期结果。
我希望我已经很好地传达了这个问题!
我相信你正试图用铁路机车征服大海。 虽然还是有可能达到你想要的,但从仙丹的角度来说,这完全是错误的,不管它是什么意思。
首先,没有“上下文”的概念。 在所有。 你所拥有的一切都只是普通的好函数。 如果您坚持使用“上下文”一词,则有两种上下文:编译和运行时。
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
将为其显示什么,它就会起作用。 例如
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"}}
您可能会使用这种技术来实现您想要的,但它没有多大意义,因为生成的整个结构将来无法修改。 条款是一成不变的,记住它。
希望它能澄清事情。 如果您仍然好奇,请随时提出更多问题。
感谢@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。 最后,复杂性不值得。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.