为什么Elixir中有两种功能?

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

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

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

有两种类型的函数并从一种类型转换为另一种类型以便将它们传递给其他函数有任何好处吗?使用两种不同的表示法调用函数是否有好处?

30565 次浏览

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

让我们从第二个开始,fnfn是一个闭包,类似于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中的函数是用而且来标识的。因此,需要两个参数的函数与需要三个参数的函数是不同的,即使它们具有相同的名称。因此,如果我们简单地传递hello,我们将不知道你实际指的是哪个hello。有两个,三个或四个参数的那个?这与我们不能用不同属性的子句创建匿名函数的原因完全相同。

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

&hello/1

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

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

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

我不知道这对其他人是否有用,但我最终理解这个概念的方式是意识到长生不老药函数并不是函数。

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

MyModule.my_function(foo)

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

fn或&作为函数指针的符号,但实际上它的功能要多得多。这是对周围环境的封闭。

如果你问自己:

在这里,我需要一个执行环境还是一个数据值?

如果你需要使用fn执行,那么大部分困难就会变得很大 清晰。< / p >

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

两种类型的函数

如果一个模块包含这个:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).
你不能只是把它剪切粘贴到外壳中,然后得到相同的结果 结果。< / p >

这是因为Erlang有一个bug。Erlang中的模块是序列 形式。Erlang shell计算一个序列 < / em >表达式。在Erlang中形式不是表达式

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部分是隐式的,所以它应该 就叫f(10)

如果你就这样让它度过接下来的二十年

我可能是错的,因为没有人提到它,但我也有这样的印象,其原因也是ruby的遗产,即能够调用没有括号的函数。

显然涉及到Arity,但让我们先把它放在一边,使用没有参数的函数。在像javascript这样必须使用括号的语言中,很容易区分将函数作为参数传递和调用函数。只有在使用括号时才调用它。

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


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

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

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

最后一个问题是,有时候你必须在代码中调用它们,因为它们并不总是与迭代器类型的函数一起使用,或者你可能自己编写了迭代器。对于这个小故事,由于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,我喜欢没有括号的函数调用,但像你一样,我发现结果有时相当误导。

在长生不老药中,它只是一个额外的点,而在红宝石中,你在这个上面有块。block很神奇,我很惊讶你可以用block做多少事情,但它们只在你只需要一个匿名函数的时候起作用,也就是最后一个参数。然后,由于您应该能够处理其他场景,这里就出现了整个方法/lambda/proc/块的混乱。

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

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

这实际上只是结合了ruby风格的“不带paren的函数执行”的实际情况的一个非常小的区别。

比较:

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提示。想象一下,如果每个变量去引用都必须检查一个函数?或者,想象一下,总是推断变量解引用使用的是函数需要什么样的英雄主义?

只有第二种函数似乎是一类对象,可以作为参数传递给其他函数。模块中定义的函数需要封装在fn中。有一些语法糖,看起来像otherfunction(myfunction(&1, &2)),为了使这更容易,但为什么它是必要的摆在第一位?为什么不能直接做otherfunction(myfunction))?

你可以做otherfunction(&myfunction/2)

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

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

fn ->语法用于使用匿名函数。使用var.()只是告诉elixir,我想让你使用带有func的var并运行它,而不是将var引用为仅保存该函数的东西。

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

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

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

在elixir中,我们使用def来简单地定义函数,就像我们在其他语言中做的那样。 fn创建一个匿名函数,参考以获得更多说明