erlang 为什么Elixir有两种功能?

dhxwm5r4  于 2022-12-08  发布在  Erlang
关注(0)|答案(9)|浏览(203)

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

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

只有第二种函数看起来是第一类对象,可以作为参数传递给其他函数。在模块中定义的函数需要 Package 在fn中。为了简化这一点,有一些类似otherfunction(&myfunction(&1, &2))的语法。但是为什么一开始就必须这样做呢?为什么我们不能直接使用otherfunction(myfunction))呢?是不是只允许像Ruby中那样调用没有括号的模块函数呢?它似乎继承了Erlang的这个特性,Erlang也有模块函数和乐趣,那么它实际上是来自Erlang VM内部的工作方式吗?
拥有两种类型的函数并将它们从一种类型转换为另一种类型以便传递给其他函数是否有好处?拥有两种不同的符号来调用函数是否有好处?

6fe3ivhb

6fe3ivhb1#

为了澄清命名,它们都是函数。一个是命名函数,另一个是匿名函数。但你是对的,它们的工作方式有些不同,我将说明它们为什么会这样工作。
让我们从第二个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

哦,不!我们得到了一个错误!我们不能混合期望不同数量的参数的子句。一个函数总是有一个固定的arity。
现在,让我们来讨论命名函数:

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中的函数是通过name和arity来标识的。所以一个期望两个参数的函数和一个期望三个参数的函数是不同的,即使它们有相同的名称。所以如果我们简单地传递hello,我们不知道你指的是哪个hello,是两个,三个还是四个参数的hello?这和我们不能创建一个匿名函数的原因是一样的。
从Elixir v0.10.1开始,我们有一个语法来捕获命名函数:

&hello/1

这将捕获arity为1的本地命名函数hello。在整个语言及其文档中,使用hello/1语法标识函数是非常常见的。
这也是Elixir使用点来调用匿名函数的原因。因为你不能简单地将hello作为函数传递,而是需要显式地捕获它,所以在命名函数和匿名函数之间有一个自然的区别,调用每个函数的不同语法使一切都更显式(Lispers应该很熟悉这一点,因为Lisp 1和Lisp 2的讨论)。
总的来说,这就是为什么我们有两个函数以及它们的行为不同的原因。

n3ipq98p

n3ipq98p2#

我不知道这对其他人有多大用处,但我最终理解这个概念的方式是意识到长生不老药的功能不是功能。
长生不老药里的一切都是一种表达

MyModule.my_function(foo)

不是一个函数,而是通过执行my_function中的代码返回的表达式。实际上,只有一种方法可以获得一个“Function”,并将其作为参数传递,那就是使用匿名函数表示法。
把fn或&符号当作函数指针是很有吸引力的,但实际上它的作用远不止于此,它是对周围环境的封闭。
如果你问自己:

  • 在此位置需要执行环境或数据值吗?*

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

kkih6yb8

kkih6yb83#

我可能是错的,因为没有人提到它,但我也有这样的印象,这也是因为能够调用没有括号的函数的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)

这也是一个设计上的选择,elixel不是面向对象的,因此调用肯定不会使用第一种形式,我不能代表José,但看起来elixel中使用了第二种形式,因为它看起来仍然像是一个带有额外字符的函数调用,它足够接近函数调用。
我没有考虑所有的利弊,但看起来在这两种语言中,只要你把括号作为匿名函数的强制性条件,你就可以只使用括号。看起来是这样的:
强制性括号VS略有不同的符号
在这两种情况下,你都是例外,因为你让两者的行为不同。既然有区别,你最好让它显而易见,并使用不同的符号。强制性的括号在大多数情况下看起来很自然,但当事情没有按计划进行时,就会很混乱。
给你。现在这可能不是世界上最好的解释,因为我简化了大部分的细节。而且大部分都是设计选择,我试图给予他们一个理由,而不是判断他们。我喜欢长生不老药,我喜欢Ruby,我喜欢没有括号的函数调用,但像你一样,我发现结果相当误导偶尔。
在elixir中,它只是一个额外的点,而在ruby中,你在这个点上有块。块是很神奇的,我很惊讶你能用块做很多事情,但是它们只在你只需要一个匿名函数,也就是最后一个参数的时候才起作用。然后因为你应该能够处理其他的情况,这里出现了整个方法/lambda/proc/block的混乱。
不管怎样...这超出了范围。

6bc51xsx

6bc51xsx4#

我一直不明白为什么解释起来这么复杂。
这实际上只是一个非常小的区别,再加上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提示。想象一下,如果每个变量解引用都必须检查一个函数?或者,想象一下,如果总是推断变量解引用正在使用一个函数,需要什么样的勇气?

pdsfdshx

pdsfdshx5#

关于这种行为,有一篇很棒的博客文章:link

两种类型的函数

如果模块包含以下内容:

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

你不能只是剪切和粘贴到 shell ,并得到同样的结果。
这是因为Erlang中有一个bug。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

这两者是不一样的。这一点愚蠢一直是二郎神,但我们没有注意到它,我们学会了与它共存。

调用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)。
如果你就这样离开它,期望用你生命中接下来的二十年来解释为什么。

idv4meu8

idv4meu86#

Elixir为函数提供了可选的大括号,包括元数为0的函数。让我们看一个例子来说明为什么它使单独的调用语法变得重要:

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的含义:获取函数本身、调用它或同时调用生成的匿名函数。

1yjd4xko

1yjd4xko7#

fn ->语法是用来使用匿名函数的。执行var.()只是告诉elixir我想让你把那个带函数的var取出来并运行它,而不是把var当作只包含那个函数的东西。
Elixir有一种常见的模式,我们不需要在函数内部使用逻辑来查看某个东西应该如何执行,而是根据输入的类型来匹配不同的函数。我想这就是为什么我们在function_name/1的意义上用arity来引用东西的原因。
习惯于做速记函数定义(func(&1)等)有点奇怪,但当你试图用管道或保持代码简洁时,这很方便。

ia2d9nvy

ia2d9nvy8#

在elixi中,我们使用def来简单地定义一个函数,就像我们在其他语言中所做的那样。fn创建一个匿名函数。

z18hc3ub

z18hc3ub9#

只有第二种函数看起来是第一类对象,可以作为参数传递给其他函数。在模块中定义的函数需要用fn Package 。为了简化这一点,有一些语法糖,看起来像otherfunction(myfunction(&1, &2)),但为什么它是必要的呢?为什么我们不能直接用otherfunction(myfunction))呢?
你可以做otherfunction(&myfunction/2)
由于elixir可以执行没有括号的函数(如myfunction),使用otherfunction(myfunction))时,它将尝试执行myfunction/0
因此,需要使用捕获操作符并指定函数(包括arity),因为可以有不同的函数具有相同的名称,例如&myfunction/2

相关问题