长生不老药! ...如何从调用者的范围中读取变量

Elixir var! ... How to read a variable from caller's scope

抱歉,如果有人问过这个问题。在论坛中搜索 var!,我得到了所有带有 var 字样的 post。很难缩小范围。

努力编写一个从调用者的上下文中读取变量并从函数中 returns 读取变量的宏。这是我能想到的最简单的问题形式:

defmodule MyUnhygienicMacros do

  defmacro create_get_function do
    quote do
      def get_my_var do
        var!(my_var)
      end
    end
  end

end

defmodule Caller do

  require MyUnhygienicMacros

  my_var = "happy"

  MyUnhygienicMacros.create_get_function()

end

目标是在我 运行 iex 会话时看到这个:

$ Caller.get_my_var()
"happy"

但这不编译。来电者的 my_var 也未使用。

编译错误expected "my_var" to expand to an existing variable or be part of a match.

我读过 McCord 的元编程书籍、此博客 post (https://www.theerlangelist.com/article/macros_6) 和许多其他书籍。看起来应该可以,但我就是想不通为什么不行..

查看这些文档:https://hexdocs.pm/elixir/1.12/Kernel.SpecialForms.html#quote/2

有很多例子。 my_var 仅在 quote 块内定义,但不在 quote 块内的 def 函数内定义。

你可以这样做:

defmodule MyUnhygienicMacros do
  defmacro create_get_function() do
    quote do
      @my_var var!(my_var)
      def get_my_var do
        @my_var
      end
    end
  end
end

defmodule Caller do
  require MyUnhygienicMacros
  my_var = "happy"
  MyUnhygienicMacros.create_get_function()
end

Caller.get_my_var()
|> IO.inspect()

并在 quote 块内调用 var!,分配给模块属性 @my_var

但我不太擅长元编程,可能还有其他人可以更好地回答它。

如果您已经了解 Elixir 中的惯用法以及可能有悖常理的内容,请原谅我。我提出这个答案是希望它能针对您问题的精神。

首先,Elixir 中的一切都是赋值,所以在大多数情况下,不可能读取超出创建范围的变量。在我做 OO 编程的日子里,最难忘记的事情可能是简单的模式(看起来很像你问题中的代码):

# pseudo-code
x = "something"
foreach y in x {
  x = "something new"
}

这种类型的结构在 Elixir 中不起作用——您经常需要使用一些 map 或 reduce 函数来完成等效的结果。您可以通过使用宏来绕过此限制,但可能必须有一个非常好的理由。因此,也许您应该重新考虑为什么需要这样的结构,或者您至少可以分享理由,以便阅读您的问题的其他人清楚。

其次,考虑在需要值时将参数传递给您的宏——这有助于使范围更加明显。您可以使用 unquote 访问它们,例如

defmodule Foo do
  defmacro __using__(opts) do
    quote do 
      def get_thing(), do: unquote(opts[:thing])
    end
  end
end

所以

defmodule Bar do
  use Foo, thing: "blort"
end

defmodule Glop do
  use Foo, thing: "eesh"
end

将允许你做这样的事情:

Bar.get_thing() |> IO.puts() # "blort"
Glop.get_thing() |> IO.puts() # "eesh"

我的结论是,在您的宏中做任何过于花哨或巧妙的事情都会使它们难以调试和维护。对于它的价值,我通常发现最好让宏保持“瘦”(就像 MVC 应用程序中的瘦控制器):根据我的经验,当它们移交给其他地方的另一个常规功能时,它们工作得最好,例如

  defmacro __using__(opts) do
    quote do
      def thing(x), do: Verbose.thing(unquote(opts[:arg1]), unquote(opts[:arg2]), x)

    end
  end   

希望其中的一些想法对您的情况有用。

Kernel.var!/2 宏并不如您所想。

var!/2 的唯一目的是将变量标记为宏卫生。这意味着,使用 var!/2 可能会更改外部(关于当前上下文)范围内的变量值。在您的示例中,有 两个 范围(defmacro[create_get_function]def[get_my_var])要绕过,这就是 my_var 无法通过的原因。

整个问题看起来像 XY-Problem. It looks like you want to declare kinda compile-time variable and modify it all way through the module code. For this purpose we have module attributesaccumulate: true

如果您想在 create_get_function/0 中简单地使用此变量,只需 unquote/1 即可。如果要累积值,请使用模块属性。如果你最终还是想保持你的方式,传递本地编译时变量,两次打破卫生,对于两个范围。

defmodule MyUnhygienicMacros do
  defmacro create_get_function do
    quote do
      my_var_inner = var!(my_var)
      def get_my_var, do: var!(my_var_inner) = 42
      var!(my_var) = my_var_inner
    end
  end
end

defmodule Caller do
  require MyUnhygienicMacros
  my_var = "happy"
  MyUnhygienicMacros.create_get_function()
  IO.inspect(my_var, label: "modified?")
end

请注意,与您预期的不同,上面的代码在编译时 期间仍会打印 modified?: "happy" 。发生这种情况是因为 var!(my_var_inner) = 42 调用将一直保持到运行时,并且在这里绕过宏卫生将是一个空操作。