【问题标题】:Why are there two kinds of functions in Elixir?为什么 Elixir 有两种功能?
【发布时间】:2013-08-03 10:08:26
【问题描述】:

我正在学习 Elixir,想知道为什么它有两种类型的函数定义:

  • 使用def 在模块中定义的函数,使用myfunction(param1, param2) 调用
  • 使用fn 定义的匿名函数,使用myfn.(param1, param2) 调用

只有第二种函数似乎是一等对象,可以作为参数传递给其他函数。模块中定义的函数需要包装在fn 中。有一些语法糖看起来像 otherfunction(&myfunction(&1, &2)) 以使这变得容易,但为什么首先需要它呢?为什么我们不能只做otherfunction(myfunction))?是否只允许像在 Ruby 中那样调用没有括号的模块函数?好像是继承了 Erlang 的这个特性,Erlang 也有模块功能和乐趣,那么它实际上是来自 Erlang VM 内部的工作方式吗?

拥有两种类型的函数并从一种类型转换为另一种类型以便将它们传递给其他函数有什么好处吗?使用两种不同的符号来调用函数有什么好处吗?

【问题讨论】:

    标签: erlang elixir


    【解决方案1】:

    只是为了澄清命名,它们都是函数。一个是命名函数,另一个是匿名函数。但你是对的,它们的工作方式有些不同,我将说明它们为什么会这样工作。

    让我们从第二个 fn 开始。 fn 是一个闭包,类似于 Ruby 中的 lambda。我们可以这样创建它:

    x = 1
    fun = fn y -> x + y end
    fun.(2) #=> 3
    

    一个函数也可以有多个子句:

    x = 1
    fun = fn
      y when y < 0 -> x - y
      y -> x + y
    end
    fun.(2) #=> 3
    fun.(-2) #=> 3
    

    现在,让我们尝试一些不同的东西。让我们尝试定义期望不同数量参数的不同子句:

    fn
      x, y -> x + y
      x -> x
    end
    ** (SyntaxError) cannot mix clauses with different arities in function definition
    

    哦不!我们得到一个错误!我们不能混合使用不同数量参数的子句。函数总是有固定的数量。

    现在,我们来谈谈命名函数:

    def hello(x, y) do
      x + y
    end
    

    正如所料,他们有一个名字,他们也可以接收一些参数。但是,它们不是闭包:

    x = 1
    def hello(y) do
      x + y
    end
    

    此代码将无法编译,因为每次看到def,都会得到一个空的变量范围。这是他们之间的一个重要区别。我特别喜欢这样一个事实,即每个命名函数都以全新的方式开始,并且您不会将不同范围的变量全部混合在一起。你有一个明确的界限。

    我们可以将上面命名的 hello 函数作为匿名函数检索。你自己提到过:

    other_function(&hello(&1))
    

    然后你问,为什么我不能像其他语言那样简单地将它传递为hello?这是因为 Elixir 中的函数由名称 arity 标识。因此,期望两个参数的函数与期望三个参数的函数是不同的函数,即使它们具有相同的名称。因此,如果我们只是通过hello,我们将不知道您的实际意思是哪个hello。有两个、三个或四个参数的那个?这就是为什么我们不能创建带有不同参数的子句的匿名函数的原因。

    从 Elixir v0.10.1 开始,我们有了一个语法来捕获命名函数:

    &hello/1
    

    这将捕获具有 arity 1 的本地命名函数 hello。在整个语言及其文档中,以 hello/1 语法识别函数是很常见的。

    这也是 Elixir 使用点来调用匿名函数的原因。由于您不能简单地将hello 作为函数传递,而是需要显式捕获它,命名函数和匿名函数之间存在自然区别,并且调用每个函数的独特语法使所有内容都更加明确(Lispers 将是由于 Lisp 1 与 Lisp 2 的讨论而熟悉这一点)。

    总的来说,这就是我们有两个函数以及它们行为不同的原因。

    【讨论】:

    • 我也在学习 Elixir,这是我遇到的第一个让我停顿的问题,它似乎有些不一致。很好的解释,但要清楚……这是实现问题的结果,还是反映了关于函数使用和传递的更深层次的智慧?由于匿名函数可以根据参数值进行匹配,因此能够匹配参数的数量似乎很有用(并且与其他地方的函数模式匹配一​​致)。
    • 它不是一个实现约束,因为它也可以用作f()(不带点)。
    • 您可以通过在警卫中使用is_function/2 来匹配参数的数量。 is_function(f, 2) 检查它的元数为 2。:)
    • 对于命名函数和匿名函数,我会首选不带点的函数调用。有时它会让人感到困惑,您会忘记某个特定函数是匿名的还是命名的。在咖喱方面也有更多的噪音。
    • 在 Erlang 中,匿名函数调用和常规函数在语法上已经不同:SomeFun()some_fun()。在 Elixir 中,如果我们删除点,它们将是相同的 some_fun()some_fun(),因为变量使用与函数名称相同的标识符。因此是点。
    【解决方案2】:

    我不知道这对其他人有多大用处,但我最终理解这个概念的方式是意识到 elixir 函数不是函数。

    elixir 中的一切都是一种表达。所以

    MyModule.my_function(foo) 
    

    不是函数,而是执行my_function中的代码返回的表达式。实际上只有一种方法可以获得可以作为参数传递的“函数”,那就是使用匿名函数表示法。

    将 fn 或 & 表示法称为函数指针很诱人,但实际上更多。这是对周围环境的封闭。

    如果你问自己:

    在这个位置我需要一个执行环境或一个数据值吗?

    如果你需要执行使用 fn,那么大部分的困难都会变得很大 更清晰。

    【讨论】:

    • 很明显MyModule.my_function(foo) 是一个表达式,但MyModule.my_function“可能”是一个返回函数“对象”的表达式。但是由于您需要告诉arity,因此您需要MyModule.my_function/1 之类的东西。而且我猜他们认为最好使用&amp;MyModule.my_function(&amp;1) 语法,而不是允许表达数量(并且也用于其他目的)。仍然不清楚为什么有一个用于命名函数的() 运算符和一个用于未命名函数的.() 运算符
    • 我想你想要:&amp;MyModule.my_function/1 这就是你如何将它作为函数传递。
    【解决方案3】:

    我可能是错的,因为没有人提到它,但我也觉得这样做的原因也是 ruby​​ 能够在没有括号的情况下调用函数的传统。

    Arity 显然参与其中,但让我们把它放在一边,使用不带参数的函数。在像 javascript 这样强制使用括号的语言中,很容易区分将函数作为参数传递和调用函数。只有在使用方括号时才调用它。

    my_function // argument
    (function() {}) // argument
    
    my_function() // function is called
    (function() {})() // function is called
    

    如您所见,命名与否并没有太大的区别。但是 elixir 和 ruby​​ 允许您在没有括号的情况下调用函数。这是我个人喜欢的设计选择,但它有这样的副作用,你不能只使用没有括号的名称,因为这可能意味着你想调用函数。这就是&amp; 的用途。如果您暂时离开 arity appart,在函数名称前加上 &amp; 意味着您明确希望将此函数用作参数,而不是此函数返回的内容。

    现在匿名函数有点不同,它主要用作参数。同样,这是一种设计选择,但其背后的原因是它主要由迭代器类型的函数使用,这些函数将函数作为参数。所以显然你不需要使用&amp;,因为默认情况下它们已经被视为参数。这是他们的目的。

    现在最后一个问题是,有时您必须在代码中调用它们,因为它们并不总是与迭代器类型的函数一起使用,或者您可能正在自己编写迭代器。对于这个小故事,由于 ruby​​ 是面向对象的,主要的方法是在对象上使用call 方法。这样,您可以保持非强制性括号行为一致。

    my_lambda.call
    my_lambda.call()
    my_lambda_with_arguments.call :h2g2, 42
    my_lambda_with_arguments.call(:h2g2, 42)
    

    现在有人想出了一个快捷方式,基本上看起来像一个没有名字的方法。

    my_lambda.()
    my_lambda_with_arguments.(:h2g2, 42)
    

    同样,这是一种设计选择。现在 elixir 不是面向对象的,因此肯定不使用第一种形式。我不能代表 José,但它看起来像在 elixir 中使用了第二种形式,因为它看起来仍然像一个带有额外字符的函数调用。它已经足够接近函数调用了。

    我没有考虑所有的利弊,但看起来在这两种语言中,只要您对匿名函数强制使用括号,您就可以只使用括号。好像是这样的:

    强制括号 VS 符号略有不同

    在这两种情况下,您都会例外,因为您使两者的行为不同。由于存在差异,您不妨让它变得明显并采用不同的符号。强制括号在大多数情况下看起来很自然,但当事情没有按计划进行时会非常混乱。

    给你。现在这可能不是世界上最好的解释,因为我简化了大部分细节。而且其中大部分是设计选择,我试图在不判断它们的情况下给出它们的理由。我喜欢 elixir,我喜欢 ruby​​,我喜欢不带括号的函数调用,但是像你一样,我偶尔会发现结果很误导。

    在长生不老药中,它只是这个额外的点,而在 ruby​​ 中,你在它上面有块。块是惊人的,我很惊讶你可以用块做多少,但它们只在你只需要一个匿名函数(即最后一个参数)时才起作用。那么既然你应该能够处理其他场景,那么整个方法/lambda/proc/block的混乱就来了。

    无论如何...这超出了范围。

    【讨论】:

      【解决方案4】:

      我一直不明白为什么对此的解释如此复杂。

      这实际上只是与 Ruby 风格的“无括号的函数执行”的现实相结合的一个非常小的区别。

      比较:

      def fun1(x, y) do
        x + y
      end
      

      收件人:

      fun2 = fn
        x, y -> x + y
      end
      

      虽然这两个都只是标识符...

      • fun1 是一个标识符,用于描述使用 def 定义的命名函数。
      • fun2 是描述变量的标识符(恰好包含对函数的引用)。

      当您在其他表达式中看到fun1fun2 时,考虑一下这意味着什么?在评估该表达式时,您是调用引用的函数还是仅引用内存不足的值?

      在编译时没有好办法知道。 Ruby 可以自省变量命名空间,以查明变量绑定是否在某个时间点遮蔽了函数。正在编译的 Elixir 并不能真正做到这一点。这就是点符号的作用,它告诉 Elixir 它应该包含一个函数引用并且应该调用它。

      这真的很难。想象一下,没有点符号。考虑这段代码:

      val = 5
      
      if :rand.uniform < 0.5 do
        val = fn -> 5 end
      end
      
      IO.puts val     # Does this work?
      IO.puts val.()  # Or maybe this?
      

      鉴于上面的代码,我想很清楚为什么你必须给 Elixir 提示。想象一下,如果每个变量取消引用都必须检查一个函数?或者,想象一下需要什么英雄才能总是推断出变量取消引用正在使用函数?

      【讨论】:

        【解决方案5】:

        有一篇关于这种行为的优秀博文:link

        两种功能

        如果一个模块包含这个:

        fac(0) when N > 0 -> 1;
        fac(N)            -> N* fac(N-1).
        

        您不能只是将其剪切并粘贴到外壳中并得到相同的结果 结果。

        这是因为 Erlang 有一个 bug。 Erlang 中的模块是序列 表格。 Erlang shell 计算一个序列 表达式。在 Erlang 中,FORMS 不是 EXPRESSIONS

        double(X) -> 2*X.            in an Erlang module is a FORM
        
        Double = fun(X) -> 2*X end.  in the shell is an EXPRESSION
        

        两者不一样。这点傻事一直是 Erlang 永远,但我们没有注意到它,我们学会了忍受它。

        打点电话fn

        iex> f = fn(x) -> 2 * x end
        #Function<erl_eval.6.17052888>
        iex> f.(10)
        20
        

        在学校我学会了通过写 f(10) 而不是 f.(10) 来调用函数 - 这“真的”是一个名称类似于 Shell.f(10) 的函数(它是 shell中定义的函数)shell部分是隐式的,所以它应该 就叫 f(10)。

        如果你这样离开,预计接下来的二十年 你的生活解释了为什么。

        【讨论】:

        • 我不确定直接回答 OP 问题是否有用,但提供的链接(包括 cmets 部分)对于问题的目标受众(即那些我们刚接触和学习 Elixir+Erlang。
        • 链接现在失效了:(
        【解决方案6】:

        Elixir 为函数提供了可选的大括号,包括具有 0 arity 的函数。让我们看一个示例,说明为什么它使单独的调用语法很重要:

        defmodule Insanity do
          def dive(), do: fn() -> 1 end
        end
        
        Insanity.dive
        # #Function<0.16121902/0 in Insanity.dive/0>
        
        Insanity.dive() 
        # #Function<0.16121902/0 in Insanity.dive/0>
        
        Insanity.dive.()
        # 1
        
        Insanity.dive().()
        # 1
        

        如果不区分两种类型的函数,我们就无法说出Insanity.dive 的含义:获取函数本身、调用它,还是调用生成的匿名函数。

        【讨论】:

          【解决方案7】:

          fn -&gt; 语法用于使用匿名函数。做 var.() 只是告诉 elixir 我希望你把那个带有 func 的 var 拿走并运行它,而不是把 var 称为只是持有那个函数的东西。

          Elixir 有一个常见的模式,我们不是在函数内部有逻辑来查看应该如何执行,而是根据我们拥有的输入类型匹配不同的函数。我认为这就是为什么我们在 function_name/1 意义上通过 arity 来指代事物。

          习惯于使用简写函数定义(func(&1) 等)有点奇怪,但是当您尝试管道或保持代码简洁时很方便。

          【讨论】:

            【解决方案8】:

            只有第二种函数似乎是一等对象,可以作为参数传递给其他函数。模块中定义的函数需要包装在 fn 中。有一些语法糖看起来像otherfunction(myfunction(&amp;1, &amp;2)),以便简化它,但为什么首先需要它?为什么我们不能只做otherfunction(myfunction))

            你可以otherfunction(&amp;myfunction/2)

            由于 elixir 可以执行不带括号的函数(如 myfunction),因此使用 otherfunction(myfunction)) 它将尝试执行 myfunction/0

            因此,您需要使用捕获运算符并指定函数,包括 arity,因为您可以拥有同名的不同函数。因此,&amp;myfunction/2

            【讨论】:

              猜你喜欢
              • 2016-01-24
              • 2017-03-19
              • 2011-01-10
              • 2016-01-28
              • 2016-04-18
              • 1970-01-01
              • 1970-01-01
              • 2014-04-20
              • 1970-01-01
              相关资源
              最近更新 更多