为什么Elixir有两种功能?

Ale*_*don 270 erlang elixir

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

  • 模块中定义的函数def,调用usingmyfunction(param1, param2)
  • 定义的匿名函数fn,调用usingmyfn.(param1, param2)

只有第二种函数似乎是第一类函数,可以作为参数传递给其他函数.模块中定义的函数需要包装在一个fn.有一些语法糖似乎是otherfunction(myfunction(&1, &2))为了使这容易,但为什么它首先是必要的?我们为什么不能这样做otherfunction(myfunction))?是否只允许调用模块函数而不像Ruby那样括号?它似乎从Erlang继承了这个特性,它也具有模块功能和实用程序,所以它实际上来自Erlang VM如何在内部工作?

有两种类型的函数并从一种类型转换为另一种类型以便将它们传递给其他函数有什么好处?呼叫功能有两种不同的符号吗?

Jos*_*lim 372

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

让我们从第二个开始吧fn.fn是一个闭包,类似于lambdaRuby中的闭包.我们可以按如下方式创建它:

x = 1
fun = fn y -> x + y end
fun.(2) #=> 3
Run Code Online (Sandbox Code Playgroud)

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

x = 1
fun = fn
  y when y < 0 -> x - y
  y -> x + y
end
fun.(2) #=> 3
fun.(-2) #=> 3
Run Code Online (Sandbox Code Playgroud)

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

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

不好了!我们收到错误!我们不能混合需要不同数量参数的子句.一个函数总是有一个固定的arity.

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

def hello(x, y) do
  x + y
end
Run Code Online (Sandbox Code Playgroud)

正如所料,他们有一个名字,他们也可以收到一些论点.但是,它们不是闭包:

x = 1
def hello(y) do
  x + y
end
Run Code Online (Sandbox Code Playgroud)

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

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

other_function(&hello(&1))
Run Code Online (Sandbox Code Playgroud)

然后你问,为什么我不能hello像其他语言一样简单地传递它?那是因为Elixir中的函数是通过名称 arity 来标识的.因此,期望两个参数的函数与期望三个参数的函数不同,即使它们具有相同的名称.所以如果我们简单地通过hello,我们就不知道hello你的意思.有两个,三个或四个论点的那个?这与我们无法使用具有不同arities的子句创建匿名函数的原因完全相同.

自Elixir v0.10.1以来,我们有一个捕获命名函数的语法:

&hello/1
Run Code Online (Sandbox Code Playgroud)

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

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

总的来说,这就是为什么我们有两个功能以及为什么它们表现不同的原因.

  • 我也在学习Elixir,这是我遇到的第一个让我暂停的问题,有些事情似乎不一致.很好的解释,但要明确......这是实施问题的结果,还是反映了关于功能的使用和传递的更深层次的智慧?由于匿名函数可以基于参数值进行匹配,因此似乎能够匹配参数的数量(并且与其他地方的函数模式匹配一​​致)也是有用的. (28认同)
  • 在Erlang中,匿名函数调用和常规函数在语法上已经不同:`SomeFun()`和`some_fun()`.在Elixir中,如果我们删除了点,它们将是相同的`some_fun()`和`some_fun()`因为变量使用与函数名相同的标识符.因此点. (26认同)
  • 对于命名和匿名函数,我更喜欢没有点的函数调用.它有时让人感到困惑,你忘了特定的函数是匿名的还是命名的.在卷曲方面也有更多的噪音. (20认同)
  • 它不是一个实现约束,因为它也可以作为`f()`(没有点). (17认同)
  • 你可以在一个守卫中使用`is_function/2`来匹配参数的数量.`is_function(f,2)`检查它是否为2. :) (6认同)
  • @JoséValim我在邮件列表上找到了相关的讨论,你有什么结论吗?就个人而言,我喜欢在0-arity调用上强制执行()并删除`.`.如果命名函数被强制执行相同的操作也会使事情更加一致,那么我也可以.或者无参数调用的替代语法如何?LiveScript使用了一个bang:`function!` (3认同)
  • @JoséValim你说`def`获得了一个新的变量范围,实际上`fn`也获得了一个新的范围.`x = 2 fun = fn y当y <0 - > x = 4 end fun.( - 2)IO.puts x#=> 2`它不会改变x值.在我看来,在`fn`中我们定义了一个新的变量`x`,所以外部的`x`是孤立的. (3认同)
  • 对。def获得一个新的* empty *范围。我会修改。谢谢! (2认同)
  • *“这也是Elixir使用点来调用匿名函数的原因。” *以当前形式给出的答案并不能真正解释为什么Elixir在调用函数时需要点。从Arity的角度来看,我理解了匿名函数在命名函数比较中的局限性。我也了解为什么Elixir在传递函数时具有语法“ hello / 1”。但是我看不到这些概念在调用函数时如何解释点。另外,如果函数`f`采用函数`g`(匿名或其他方式)作为参数,那么`f`将如何调用`g`?是否使用*点*? (2认同)
  • @mljrg:*“因此,在Elixir中需要一些东西来区分它们,为此,选择了点。” * ...为什么Elixir需要区分它们(从调用的角度来看)?为什么用匿名函数或命名函数创建它都无关紧要? (2认同)
  • @Nawaz我相信它与解析有关。点可以帮助编译器区分函数和包含对函数引用的变量。在Erlang中,您不需要这样做,因为字母大小写可以区分与声明的函数相关的小写名称和包含对函数的引用的大写变量。区分这两种情况的需要并不是Elixir和Erlang独有的。例如,Common Lisp需要使用“ FUNCALL”来调用匿名函数。Scheme和Haskell等其他语言没有区别。 (2认同)
  • @Nawaz.在Elixir中必须存在一些困难或坚定的设计决策.无论如何,我已经接受了大多数语言中"按原样"的语法,而不是抱怨太多; 事实上,我开始欣赏Elixir语法"按原样".你真的必须开始使用一段时间来忘记它的"语法细微差别".在函数`f`的情况下,如果它在参数`g`中得到一个函数,你必须把它称为`g.(...)`. (2认同)

Fre*_*Dog 17

我不知道这会对其他人有多大帮助,但我最终围绕这个概念的方式是认识到elixir函数不是函数.

灵丹妙药中的一切都是一种表达方式.所以

MyModule.my_function(foo) 
Run Code Online (Sandbox Code Playgroud)

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

将fn或&符号称为函数指针很有吸引力,但它实际上更多.这是周围环境的封闭.

如果你问自己:

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

如果你需要执行使用fn,那么大多数困难会变得更加清晰.

  • 很明显,`MyModule.my_function(foo)`是一个表达式,但是`MyModule.my_function`"可能"是一个返回函数"object"的表达式.但是既然你需要告诉arity,你需要一些像`MyModule.my_function/1`这样的东西.而且我猜他们认为最好使用`&MyModule.my_function(&1)`语法代替允许表达arity(并用于其他目的).还不清楚为什么命名函数有一个`()`运算符和一个未命名函数的`.()`运算符 (2认同)

小智 13

我永远不明白为什么对此的解释如此复杂.

它实际上只是一个非常小的区别,结合了Ruby风格的"没有parens的函数执行"的现实.

相比:

def fun1(x, y) do
  x + y
end
Run Code Online (Sandbox Code Playgroud)

至:

fun2 = fn
  x, y -> x + y
end
Run Code Online (Sandbox Code Playgroud)

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

  • 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?
Run Code Online (Sandbox Code Playgroud)

鉴于上面的代码,我认为为什么你必须给Elixir提示是非常清楚的.想象一下,如果每个变量去引用必须检查一个函数?或者,想象一下,总是推断变量解引用是使用函数需要什么样的英雄主义?


Mig*_*g R 12

我可能是错的,因为没有人提到它,但我也有这样的印象,其原因也是能够在没有括号的情况下调用函数的红宝石遗产.

Arity显然参与其中,但是暂时放下它并使用没有参数的函数.在javascript这样的语言中,括号是必需的,很容易区分传递函数作为参数和调用函数.只有在使用括号时才调用它.

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

my_function() // function is called
(function() {})() // function is called
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,命名与否并没有太大的区别.但是elixir和ruby允许你在没有括号的情况下调用函数.这是我个人喜欢的设计选择,但它有这种副作用,你不能只使用没有括号的名称,因为它可能意味着你想要调用该功能.这就是&它的用途.如果您将arity appart保留一秒钟,则在您的函数名称前加上&意味着您明确要将此函数用作参数,而不是此函数返回的内容.

现在匿名函数有点不同,因为它主要用作参数.这又是一个设计选择,但它背后的理性是它主要由迭代器使用的函数作为参数.显然你不需要使用它,&因为默认情况下它们已经被认为是参数.这是他们的目的.

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

my_lambda.call
my_lambda.call()
my_lambda_with_arguments.call :h2g2, 42
my_lambda_with_arguments.call(:h2g2, 42)
Run Code Online (Sandbox Code Playgroud)

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

my_lambda.()
my_lambda_with_arguments.(:h2g2, 42)
Run Code Online (Sandbox Code Playgroud)

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

我没有考虑所有的优点和缺点,但看起来在两种语言中,只要你为匿名函数强制使用括号,你就可以使用括号.看起来好像是:

强制性括号VS略有不同的表示法

在这两种情况下,您都会出现异常,因为两者的行为都不同.由于存在差异,您可以将其显而易见并使用不同的符号.在大多数情况下,强制性括号看起来很自然,但是当事情没有按计划进行时会非常混乱.

干得好.现在这可能不是世界上最好的解释,因为我简化了大部分细节.其中大部分都是设计选择,我试图给出一个理由而不判断它们.我喜欢长生不老药,我喜欢红宝石,我喜欢没有括号的函数调用,但是和你一样,我发现偶尔会产生很多错误的后果.

在长生不老药中,它只是这个额外的点,而在红宝石中你有块.块是惊人的,我很惊讶你可以用块做多少,但它们只在你只需要一个匿名函数时工作,这是最后一个参数.然后,因为你应该能够处理其他场景,所以整个方法/ lambda/proc/block混淆了.

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


sob*_*evn 8

有关于此行为的优秀博客文章:链接

两种功能

如果模块包含:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).
Run Code Online (Sandbox Code Playgroud)

您不能将其剪切并粘贴到shell中并获得相同的结果.

这是因为Erlang中存在一个错误.Erlang中的模块是FORMS的序列.Erlang shell评估一系列 EXPRESSIONS.在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
Run Code Online (Sandbox Code Playgroud)

两者不一样.这种愚蠢一直是Erlang的永恒,但我们没有注意到它,我们学会了忍受它.

点呼 fn

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

在学校我学会了通过编写f(10)而不是f来调用函数.(10) - 这是"真的"一个名为Shell.f(10)的函数(它是在shell中定义的函数)shell部分是隐含所以它应该被称为f(10).

如果你这样离开,那么你可以在接下来的二十年中解释原因.