【问题标题】:Why is this elixir function undefined为什么这个长生不老药函数未定义
【发布时间】:2019-05-04 21:49:18
【问题描述】:

我重新开始学习 Elixir 语言。我正在尝试使用以下代码尝试 Fizzbuzz 问题的变体。

defmodule FizzBuzz.Fb4a do

  def upto(n) when n > 0 do
    fizzbuzz(n)
  end

  defp toTuple(n), do: {n, ""}

  defp toString({v,a}) do
    if String.length(a) == 0 do v else a end
  end

  defp genFB(d, s) do
    fn ({v, a}) ->
      cond do
        rem(v, d) == 0 -> {v, a+s}
        true           -> {v, a}
      end
    end
  end

  # do3 = genFB(3, "Fizz")
  # do5 = genFB(5, "Buzz")
  # do7 = genFB(7, "Bang")

  defp fizzbuzz(n) do
    1..n
    |> Enum.map(&toTuple/1)
    # |> Enum.map(&do3/1)
    # |> Enum.map(&do5/1)
    # |> Enum.map(&do7/1)
    |> Enum.map(&toString/1)
  end

end

当我取消注释 do3 = genFB(3, "Fizz") 行时,我收到以下错误:

** (CompileError) lib/fib4a.ex:22: undefined function genFB/2

我不明白genFB/2 是如何被编译器未定义或看不到的。我显然在某处的函数定义中遗漏了一些非常基本的东西。我错过了什么?

【问题讨论】:

    标签: elixir


    【解决方案1】:

    编译阶段

    我不明白genFB/2 是如何被编译器未定义或看不到的。

    关于Elixir,人们应该清楚了解的主要一点:与许多其他语言不同,它对“元编程”和代码本身使用相同的语法。作为在 VM 中运行的编译语言,Elixir 无法执行任意代码代码必须预先编译。

    编译基本上意味着将代码转换为AST然后转换为BEAM。

    在最顶层范围(以及defmodule 宏内部)中找到的代码正在编译阶段执行。 它不包含在生成的 BEAM 中。考虑以下示例:

    defmodule Test do
      IO.puts "?️ Compilation stage!"
    
      def yo, do: IO.puts "⚡ Runtime!"
    end
    
    Test.yo
    

    如果您尝试编译它,您将看到"?️ Compilation stage!" 仅在编译期间打印。无法从生成的 BEAM 中引用此代码,因为它在编译期间执行后被简单地丢弃。

    OTOH,要打印 "⚡ Runtime!" 字符串,您需要在运行时显式运行 Test.yo

    也就是说,您的 doN 变量(即使它们引用有效的又名可用/已知的编译器函数)在编译阶段被分配为局部变量并立即丢弃,因为没有人使用它们.

    解决方法 1

    运行时函数中有一些可用的东西:

    模块属性

    它们之所以可用,是因为编译器在看到模块属性和/或宏时,将生成的 AST 就地注入,而不涉及它。考虑以下示例:

    defmodule Test do
      @mod_attr &IO.puts/1
    
      def yo, do: @mod_attr.("⚡ Runtime!")
    end
    
    Test.yo
    

    这里我们声明了模块属性,引用了函数IO.puts/1从运行时调用它。

    我们引用的函数必须在它被引用的时候编译

    考虑以下示例。

    defmodule Test do
      defmacrop puts(what), do: IO.puts(what)
    
      def yo, do: puts("?️ Compilation time!")
    end
    

    等等,什么? 它是在编译阶段打印出来的!是的。宏注入由它们的do: 块产生的AST,因此IO.puts(what)在编译阶段被执行

    要解决这个问题,应该quote 宏的内容,按原样注入而不是执行它。

    defmodule Test do
      defmacrop puts(what), do: quote(do: IO.puts(unquote(what)))
    
      def yo, do: puts("⚡ Runtime!")
    end
    
    Test.yo
    

    因此,您可能已经通过引入一个繁琐的宏、注入对真实函数的调用来修复您的代码,但我会将其排除在此答案的范围之外。完成任务还有更简单的方法。

    解决方法 2

    defmodule FizzBuzz.Fb4a do
      def upto(n) when n > 0, do: fizzbuzz(n)
    
      defp toTuple(n), do: {n, ""}
    
      defp toString({v, ""}), do: v
      defp toString({_v, a}), do: a
    
      defp genFB({v, a}, d, s) when rem(v, d) == 0, do: {v, a <> s}
      defp genFB({v, a}, _d, _s), do: {v, a}
    
      defp fizzbuzz(n) do
        1..n
        |> Enum.map(&toTuple/1)
        |> Enum.map(&genFB(&1, 3, "Fizz"))
        |> Enum.map(&genFB(&1, 5, "Bazz"))
        |> Enum.map(&genFB(&1, 7, "Bang"))
        |> Enum.map(&toString/1)
      end
    end
    

    我已经稍微清理了代码以使用模式匹配而不是命令式ifcond 子句。

    首先,您不需要返回函数。 Elixir 有一个漂亮的功能,允许使用&amp; 捕获函数。您甚至可以将 curried 函数的结果分配给一个变量并稍后调用它,但这里并不真正需要。

    如果你还想分配中间变量,请确保函数所属的模块已经编译

    defmodule FizzBuzz.Fb4a do
      defmodule Gen do
        def genFB({v, a}, d, s) when rem(v, d) == 0, do: {v, a <> s}
        def genFB({v, a}, _d, _s), do: {v, a}
      end
    
      def upto(n) when n > 0, do: fizzbuzz(n)
    
      defp toTuple(n), do: {n, ""}
    
      defp toString({v, ""}), do: v
      defp toString({_v, a}), do: a
    
      defmacrop do3, do: quote(do: &Gen.genFB(&1, 3, "Fizz"))
      defmacrop do5, do: quote(do: &Gen.genFB(&1, 5, "Bazz"))
      defmacrop do7, do: quote(do: &Gen.genFB(&1, 7, "Bang"))
    
      defp fizzbuzz(n) do
        1..n
        |> Enum.map(&toTuple/1)
        |> Enum.map(do3())
        |> Enum.map(do5())
        |> Enum.map(do7())
        |> Enum.map(&toString/1)
      end
    end
    

    希望这会有所帮助。

    【讨论】:

    • 谢谢,@AlekseiMatiushkin。这里有很多需要考虑的地方。实际上,我正在尝试类似部分应用的柯里化函数,但从创建并返回函数 (HOF) 的函数开始。我尝试将返回的函数存储在模块级别的变量中——然后我可以在函数中使用它——这让我很适应。我不知道编译器只是丢弃了我的模块级变量。我喜欢你的解决方法 2,它类似于我也想出的东西。我将不得不研究你的解决方法 3。
    【解决方案2】:

    我不明白 genFB/2 是如何未定义或不被 编译器。我显然错过了一些非常基本的东西 在某处定义函数。我错过了什么?

    这个例子也不行:

    defmodule My do
      def greet do
        IO.puts "hello"
      end
    
      greet()
    end
    

    在 iex 中:

    ~/elixir_programs$ iex my.exs
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    ** (CompileError) my.exs:6: undefined function greet/0
    

    同样的错误:

    defmodule My do
      def greet do
        IO.puts "hello"
      end
    
      My.greet()
    end
    

    原因是定义函数并没有在模块范围内引入名称,并且您在模块范围内调用函数(出于某种无法解释的原因)。

    命名函数和模块

    ...命名函数有几个特点。

    首先,定义一个命名函数不会引入一个新的绑定到 当前范围:

    defmodule M do
      def foo, do: "hi"
    
      foo()  # will cause CompileError: undefined function foo/0
    end
    

    第二,命名函数不能直接访问[the]周围的范围。

    Scoping Rules in Elixir

    def 不在模块范围内创建名称这一事实应该让您想知道如何在另一个函数中调用它们。

    这个问题的答案在于 Elixir 遵循的以下规则: 尝试将标识符解析为其值:

    任何未绑定的标识符都被视为本地函数调用。

    嗯?翻译:你不能在模块范围内调用defs——就是这样!

    我重新开始学习 Elixir 语言。

    您可以像这样在 .exs 文件中执行语句:

    defmodule My do
      def greet do
        IO.puts "hello"
      end
    
    end
    
    My.greet()  #<====  This will execute
    

    ~/elixir_programs$ iex my.exs
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    hello   #<=== Output
    Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
    iex(1)> 
    

    但如果 greet/0 是私有的 (defp),你甚至不能这样做。

    (你为什么将模块命名为FizzBuzz.Fb4a,这使得输入时尽可能烦人?命名F4有什么问题?)

    编辑:在我看来,defmodule 创建了以下范围:

    defmodule My do
      x = 100
    
      def greet do
        x = 1
      end
    
      def go do
        x = 3
      end
    end
    
      ||
      VV
    
    +----MyModuleScope-------+ 
    |                        |
    |    x = 100             |  
    |                        |
    |    +--greetScope-+     |      +--Invisible Scope--+
    |    |   x = 1    -|-----|----->|       greet       |
    |    +-------------+     |      |       go          |
    |                        |      +-------------------+
    |    +--goScope----+     |               ^    
    |    |   x = 3    -|-----|---------------+
    |    +-------------+     |
    |                        |
    +------------------------+
    

    您可以在此处看到内部范围无法访问模块范围:

    defmodule My do
      x = 10
    
      def greet do
        IO.puts(x) 
    
      end
    
    end
    
    My.greet()
    

    在 iex 中:

    ~/elixir_programs$ iex my.exs
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    warning: variable "x" is unused
      my.exs:2
    
    warning: variable "x" does not exist and is being expanded to "x()", please use parentheses to remove the ambiguity or change the variable name
      my.exs:5
    
    ** (CompileError) my.exs:5: undefined function x/0
        (stdlib) lists.erl:1338: :lists.foreach/2
        my.exs:1: (file)
    

    最后一部分是一个错误,说 x 未定义。但是,有一种方法可以访问模块范围内的值:

    defmodule My do
      x = 10
    
      def greet do
        x = 1
        IO.puts(unquote(x)) 
        x
      end
    
    end
    
    My.greet()
    

    在 iex 中:

    ~/elixir_programs$ iex my.exs
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    10
    Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
    iex(1)> 
    

    但是,unquote() 是一个宏,所以我认为结果只是一个编译器技巧,它与运行时没有任何关系,即您不是在外部范围内查找值,编译器只是内联编译时代码中的值。

    【讨论】:

    • 谢谢。我已经阅读了“Elixir in Action”(Manning),并且正在研究“Programming Elixir >= 1.6”(PragProg)。到目前为止,他们都没有提到这些范围规则。至于模块名称,我来自一个非常不同的环境,只是依靠以前的学习。显然,这不是为了生产目的,而是为了学习目的而分离几个不同的实现。我想我会继续学习“公认”的做法。
    • 我已经阅读了“Elixir in Action”(曼宁),并且正在研究“Programming Elixir >= 1.6”(PragProg)。 我已经知道了绕路。 他们都没有提到这些范围规则。 没有,但是每种语言都有在学习曲线上表现出来的特性。 :)
    • 再次感谢。因此,相同的范围规则也阻止在管道中使用do3do5do7 函数。如果匿名函数绑定到当前模块中的变量,您不能使用该模块中的绑定变量来调用函数吗?
    • @melston,我在帖子中添加了一张我认为已创建的范围的图。通常内部作用域可以看到外部作用域的值,但是对于模块作用域却不是这样。要访问模块范围,您必须编写 unquote(varname)unquote() 有效地表示跳转到模块范围并查找变量。
    • 是的。我看到了图表。我仍然不熟悉 unquote 或它的作用。我的印象是它被用在我还不熟悉的宏中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-30
    • 1970-01-01
    • 2016-09-01
    • 1970-01-01
    • 2016-02-01
    • 1970-01-01
    相关资源
    最近更新 更多