306

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

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

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

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

4

8 回答 8

424

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

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

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

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

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

other_function(&hello(&1))

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

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

&hello/1

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

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

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

于 2013-08-02T18:30:00.517 回答
18

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

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

MyModule.my_function(foo) 

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

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

如果你问自己:

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

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

于 2015-05-22T16:41:51.503 回答
15

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

这实际上只是一个非常小的区别,结合了 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 提示。想象一下,如果每个变量取消引用都必须检查一个函数?或者,想象一下需要什么英雄才能总是推断出变量取消引用正在使用函数?

于 2016-03-23T07:01:03.813 回答
15

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

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

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

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

如您所见,命名与否并没有太大区别。但是 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)

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

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

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

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

强制括号 VS 符号略有不同

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

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

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

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

于 2015-11-10T09:52:23.543 回答
10

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

两种功能

如果一个模块包含这个:

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

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

这是因为 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

两者不一样。这一点愚蠢一直是 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)。

如果你这样离开它,期望在你生命的下一个 20 年中解释原因。

于 2015-11-06T22:51:38.450 回答
3

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

于 2020-04-09T10:07:38.637 回答
1

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

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

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

于 2019-07-07T22:35:56.153 回答
0

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

你可以做otherfunction(&myfunction/2)

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

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

于 2017-09-14T01:02:00.180 回答