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

时间:2017-01-27 03:16:25

标签: ruby macros elixir dsl

这是我正在尝试做的事情:

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获得sweetenerTopping,而在另一方面你最终得到一个新的Topping类实例。

我可以通过构建预先嵌套而没有DSL和所有宏来解决这个问题,可以说更清晰,但我想了解如何使用Elixir宏获得预期的结果。

我希望我能够很好地传达这个问题!

2 个答案:

答案 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的提示,我得到了这份工作。正如预期的那样,它很粗糙而且很混乱,但它确实有效:

基本D​​SL:

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无状态。最后,复杂性并不值得。