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

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

Only the second kind of function seems to be a first-class object and can be passed as a parameter to other functions. A function defined in a module needs to be wrapped in a fn. There's some syntactic sugar which looks like otherfunction(&myfunction(&1, &2)) in order to make that easy, but why is it necessary in the first place? Why can't we just do otherfunction(myfunction))? Is it only to allow calling module functions without parenthesis like in Ruby? It seems to have inherited this characteristic from Erlang which also has module functions and funs, so does it actually comes from how the Erlang VM works internally?

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


当前回答

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

我们从第二个开始,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

哦,不!我们得到一个错误!我们不能混合使用期望不同数量参数的子句。一个函数总是有固定的元数。

现在,让我们讨论命名函数:

def hello(x, y) do
  x + y
end

正如预期的那样,它们有一个名称,还可以接收一些参数。然而,它们不是闭包:

x = 1
def hello(y) do
  x + y
end

这段代码将无法编译,因为每次看到def时,都会得到一个空变量作用域。这是他们之间的一个重要区别。我特别喜欢这样一个事实:每个命名函数都是从头开始的,你不会把不同作用域的变量混在一起。你有明确的界限。

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

other_function(&hello(&1))

And then you asked, why I cannot simply pass it as hello as in other languages? That's because functions in Elixir are identified by name and arity. So a function that expects two arguments is a different function than one that expects three, even if they had the same name. So if we simply passed hello, we would have no idea which hello you actually meant. The one with two, three or four arguments? This is exactly the same reason why we can't create an anonymous function with clauses with different arities.

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

&hello/1

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

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

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

其他回答

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意味着:获取一个函数本身,调用它,或者也调用结果匿名函数。

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

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

比较:

def fun1(x, y) do
  x + y
end

To:

fun2 = fn
  x, y -> x + y
end

虽然这两个都只是标识符……

Fun1是一个描述用def定义的命名函数的标识符。 Fun2是描述变量(恰好包含对函数的引用)的标识符。

当你在其他表达式中看到fun1或fun2时,这意味着什么?在计算该表达式时,是调用引用的函数还是只是引用内存外的值?

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

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

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

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

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

As you can see, naming it or not does not make a big difference. But elixir and ruby allow you to call functions without the brackets. This is a design choice which I personally like but it has this side effect you cannot use just the name without the brackets because it could mean you want to call the function. This is what the & is for. If you leave arity appart for a second, prepending your function name with & means that you explicitly want to use this function as an argument, not what this function returns.

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

最后一个问题是,有时候你必须在代码中调用它们,因为它们并不总是与迭代器类型的函数一起使用,或者你可能自己编写了迭代器。对于这个小故事,由于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/块的混乱。

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

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

两种类型的函数

If a module contains this: fac(0) when N > 0 -> 1; fac(N) -> N* fac(N-1). You can’t just cut and paste this into the shell and get the same result. It’s because there is a bug in Erlang. Modules in Erlang are sequences of FORMS. The Erlang shell evaluates a sequence of EXPRESSIONS. In Erlang FORMS are not EXPRESSIONS. double(X) -> 2*X. in an Erlang module is a FORM Double = fun(X) -> 2*X end. in the shell is an EXPRESSION The two are not the same. This bit of silliness has been Erlang forever but we didn’t notice it and we learned to live with it.

在调用fn时

Iex > f = fn(x) -> 2 * x结束 #函数< erl_eval.6.17052888 > iex > f。(10) 20. 在学校里,我学会了用f(10)而不是f.(10) -来调用函数 这是一个“真正的”函数,它的名字是Shell.f(10) shell部分是隐式的,所以它应该 记作f(10) 如果你就这样待着,接下来的二十年 用你的生命解释为什么。

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

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

MyModule.my_function(foo) 

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

fn或&符号很容易被称为函数指针,但实际上它的作用远不止于此。这是对周围环境的封闭。

如果你问自己:

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

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