为什么Elixir中有两种功能?


279

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

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

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

拥有两种类型的功能并从一种类型转换为另一种类型以将其传递给其他功能,会有什么好处?调用函数有两种不同的表示法是否有好处?

Answers:


386

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

让我们从第二个开始fnfn是一个闭包,类似于lambdaRuby中的。我们可以如下创建它:

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您的实际含义。具有两个,三个或四个参数的那个?这与为什么我们不能使用带有不同Arities的子句创建匿名函数的原因完全相同。

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

&hello/1

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

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

总体而言,这就是我们拥有两个功能以及它们表现不同的原因。


30
我也在学习Elixir,这是我遇到的第一个让我停顿的问题,这似乎有点矛盾。很好的解释,但要清楚……这是实现问题的结果,还是反映了有关功能使用和传递的更深层次的智慧?由于匿名函数可以基于参数值进行匹配,因此似乎能够对参数的数量进行匹配(并与其他地方的函数模式匹配保持一致)似乎很有用。
飓风

17
从它也可以f()(不带点)起作用的意义上来说,它不是实现约束。
何塞·Valim

6
您可以通过使用is_function/2防护来匹配参数的数量。is_function(f, 2)检查它有2 :)元数
何塞·Valim

20
对于命名函数和匿名函数,我都希望有不带点的首选函数调用。有时,这会造成混乱,您会忘记特定功能是匿名的还是命名的。当涉及到curry时,还会有更多的噪音。
CMCDragonkai 2014年

28
在Erlang中,匿名函数调用和常规函数在语法上已经不同:SomeFun()some_fun()。在Elixir中,如果我们删除点,它们将是相同的,some_fun()并且some_fun()因为变量使用与函数名相同的标识符。因此,点。
何塞·Valim

17

我不知道这对其他人有多大用处,但是我最终将这个想法付诸实践的方法是认识到万能药不是功能。

长生不老药中的一切都是表达。所以

MyModule.my_function(foo) 

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

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

如果您问自己:

在该位置是否需要执行环境或数据值?

而且,如果您需要使用fn执行,那么大多数困难就会变得更加明显。


2
显然,这MyModule.my_function(foo)是一个表达式,但是MyModule.my_function“可以”已经是返回函数“对象”的表达式。但是,由于您需要告诉Arity,因此需要类似的东西MyModule.my_function/1。而且我猜想他们认为最好使用一种&MyModule.my_function(&1) 语法来表示Arity(也可以用于其他目的)。仍然不清楚为什么会有一个()用于命名函数的.()运算符和一个用于未命名函数的运算符
RubenLaguna

我想您想要:那&MyModule.my_function/1就是如何将其作为函数传递。
jnmandal

14

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

实际上,这只是一个很小的区别,结合了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提示。试想一下,是否每个变量取消引用都必须检查功能?或者,想像一下什么才能始终推断出变量取消引用是在使用函数?


12

我可能是错的,因为没有人提到过它,但是我也给人留下了这样的印象,那就是红宝石的传统,就是能够不带括号地调用函数。

显然会涉及Arity,但让它搁置一会儿并使用不带参数的函数。在像JavaScript这样的语言中,必须使用方括号,在传递函数作为参数与调用函数之间很容易有所区别。仅在使用方括号时才称呼它。

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

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

如您所见,命名与否没有太大的区别。但是,长生不老药和红宝石允许您调用不带括号的函数。这是我个人喜欢的设计选择,但它有副作用,您不能仅使用不带方括号的名称,因为这可能意味着您要调用该函数。这就是&它的用途。如果您离开arity appart一秒钟,则在函数名称之前加上,&表示您明确希望将此函数用作参数,而不是该函数返回的值。

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

现在的最后一个问题是,有时您必须在代码中调用它们,因为它们并不总是与迭代器类型的函数一起使用,或者您可能自己在编写迭代器。对于这个小故事,由于红宝石是面向对象的,所以主要的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)

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

我没有考虑所有优点和缺点,但是看起来在两种语言中,只要将括号强制用于匿名函数,就可以不用括号了。看来是这样的:

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

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

干得好。现在这可能不是世界上最好的解释,因为我简化了大多数细节。同样,大多数都是设计选择,我尝试给出它们的理由而不对其进行判断。我喜欢长生不老药,我喜欢红宝石,我喜欢不带括号的函数调用,但是像您一样,我偶尔会发现错误的后果。

在长生不老药中,这只是这个额外的点,而在红宝石中,您在此之上有块。块很棒,让我惊讶的是,仅使用块可以做多少事情,但是它们仅在您只需要一个匿名函数(最后一个参数)时才起作用。然后,由于您应该能够处理其他情况,因此出现了整个方法/ lambda / proc / block混淆。

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


9

关于此行为,有一篇很棒的博客文章:链接

两种功能

如果模块包含以下内容:

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

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

这是因为Erlang中存在一个错误。Erlang中的模块是FORMS的序列。Erlang外壳评估一个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

两者不一样。这种愚蠢永远存在于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问题是否有用,但是提供的链接(包括评论部分)对于该问题的目标受众来说确实是一本好书,对于那些目标受众,即我们中刚接触Elixir + Erlang的人来说, 。

现在链接已死:(
AnilRedshift

2

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是什么意思:获取函数本身,调用它,或者也调用生成的匿名函数。


1

fn ->语法用于使用匿名函数。做var。()只是告诉elixir,我希望您使用带有功能的var并运行它,而不是将var称为仅持有该函数的东西。

Elixir具有这种通用模式,在该模式中,我们不是根据函数内部的逻辑来查看应如何执行某事,而是根据所拥有的输入类型来对不同的函数进行匹配。我认为这就是为什么我们在function_name/1某种意义上用礼节来指代事物。

习惯执行速记函数定义(func(&1)等)有点奇怪,但是在尝试通过管道传输或保持代码简洁时很方便。


0

仅第二种函数似乎是一类对象,并且可以作为参数传递给其他函数。模块中定义的功能需要包装在fn中。otherfunction(myfunction(&1, &2))为了使它变得简单,有一些语法糖看起来像,但是为什么首先需要它?我们为什么不能做otherfunction(myfunction))呢?

你可以做 otherfunction(&myfunction/2)

由于elixir可以在不使用方括号的情况下执行功能(如myfunction),因此使用otherfunction(myfunction))它会尝试执行myfunction/0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.