【问题标题】:Elixir var! ... How to read a variable from caller's scope长生不老药! ...如何从调用者的范围内读取变量
【发布时间】:2021-09-20 17:10:07
【问题描述】:

对不起,如果这已经被问到了。在论坛中搜索 var! 会得到所有带有单词 var 的帖子。很难缩小范围。

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

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 的元编程书籍、这篇博文 (https://www.theerlangelist.com/article/macros_6) 以及许多其他内容。似乎它应该可以工作,但我就是不知道为什么它不会..

【问题讨论】:

    标签: elixir metaprogramming hygiene


    【解决方案1】:

    Kernel.var!/2 宏并没有像你想象的那样做。

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

    整个问题看起来像XY-Problem。看起来你想声明一个编译时变量并通过模块代码修改它。为此,我们有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 调用将一直保持到运行时,并且在这里绕过宏卫生将是无效的。

    【讨论】:

    • 你对这是一个 XY 问题提出了很好的观点。我试图从这篇文章中得到两件事:1)如何解决我的实际问题(我最终使用模块属性来解决这个问题和 2)更好地理解 var!/1。我没有意识到我必须经历两个层次的背景。
    【解决方案2】:

    看看这些文档: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

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

    【讨论】:

    • 这是一个很好的开始 - 谢谢!您的代码确实有效,但现在我想知道是否有任何方法可以做到这一点,而不会使用该自定义属性污染调用者的环境。
    • 你在这里滥用var!/1。简单的 unquote/1 对于这个例子来说就足够了。
    • 我同意我的回答既奇怪又丑陋。我只是想尽可能接近 OP 代码并让它工作。有太多的方法可以让我知道最好的方法,而且我的元编程技能还不够发达。对于后代,请阅读@AlekseiMatiushkin 的答案,它比我的要好得多。
    【解决方案3】:

    如果您已经了解 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   
    

    希望那里的一些想法对您的情况有用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-02-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多