繁体   English   中英

使用可以更改块上下文的嵌套 Elixir 宏创建 DSL

[英]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 namesweetener ,而另一方面你会得到一个新的 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM