普通函数和 lambda Haskell 函数有什么区别?

问题描述

我是 Haskell 的初学者,我一直在关注电子书 Get Programming with Haskell

我正在学习 Lambda 函数的闭包,但我看不到以下代码的区别:

genIfEven :: Integral p => p -> (p -> p) -> p
genIfEven x = (\f -> isEven f x)

genIfEven2 :: Integral p => (p -> p) -> p -> p
genIfEven2 f x = isEven f x

如果有人能解释这里的确切区别是什么就好了

解决方法

在基本层面1,“普通”函数和使用 lambda 语法创建的函数之间没有真正的区别。是什么让你认为问它是什么有区别? (在您展示的特定示例中,函数采用不同顺序的参数,但其他方面相同;它们中的任何一个都可以使用 lambda 语法或“正常”语法定义)

函数是 Haskell 中的第一类值。这意味着您可以将它们传递给其他函数,将它们作为结果返回,在数据结构中存储和检索它们,等等。就像您可以使用数字、字符串或任何其他值一样。

就像数字、字符串等一样,使用表示函数值的语法很有帮助,因为您可能希望在其他代码中间创建一个简单的语法。例如,如果您需要将 x + 1 传递给某个函数,而您不能只为第一个写字面量 1,而是必须转到文件中的其他位置,这将是非常可怕的添加 one = 1 绑定,以便您可以返回并编写 x + one。以完全相同的方式,您可能需要将一个用于加 1 的函数传递给其他函数;在文件的其他地方添加一个单独的定义 plusOne x = x + 1 会很烦人,所以 lambda 语法为我们提供了一种编写“函数文字”的方法:\x -> x + 1.2

考虑“正常”的函数定义语法,像这样:

incrementAllBy _ [] = []
incrementAllBy n (x:xs) = (x + n) : xs 

这里我们没有任何仅表示 incrementAllBy 是其名称的函数值的源代码。函数在这种语法中隐含,分布在(可能)多个“规则”上,这些“规则”说明我们的函数返回的值,因为它被应用于某种形式的参数。这种语法也从根本上迫使我们将函数绑定到一个名称。所有这些都与 lambda 语法形成对比,后者仅直接表示函数本身,没有捆绑案例分析或名称。

然而,它们都只是写下函数的不同方式。定义后,函数之间没有区别,无论您使用哪种语法来表达它们。


您提到您正在学习闭包。目前还不清楚这与问题有什么关系,但我猜这有点令人困惑。

这里我要说一些有点争议的东西:你不需要了解闭包。3

闭包是使 incrementAllBy n xs = map (\x -> x + n) xs 之类的东西工作所涉及的内容。此处创建的函数 \x -> x + n 依赖于 n,它是一个参数,因此每次调用 incrementAllBy 时它都可能不同,并且可以同时运行多个此类调用。因此,这个 \x -> x + n 函数不能像顶级函数那样最终成为程序二进制文件中特定地址处的一段编译代码。传递给 map 的内存结构必须存储 n 的副本或存储对它的引用。这样的结构称为“闭包”,据说已经“封闭”了n,或“捕获”了它。

在 Haskell 中,您不需要知道任何这些。我认为表达式 \n -> x + n 只是创建一个新的函数值,这取决于值 n(还有值 +,它也是一等值!)在范围内。我认为您不需要以任何不同的方式思考这一点,就像您认为表达式 x + n 根据本地 n 创建一个新的数值一样。 Haskell 中的闭包仅在您尝试了解该语言的实现方式时才重要,而在您使用 Haskell 编程时则无关紧要。

闭包在命令式语言中确实很重要。 \x -> x + n 是否(相当于)存储对 n 的引用或 n 的副本(以及复制的时间和深度)的问题对于理解如何使用此函数的代码有效,因为 n 不仅仅是一种引用值的方式,它还是一个随时间(可能)具有不同值的变量。

但是在 Haskell 中,我真的不认为我们应该教初学者关于闭包的术语或概念。它使“您可以从现有值中创建新函数,即使是仅在局部范围内的函数”,这非常复杂。

因此,如果您已将这两个函数作为示例来尝试说明闭包的概念,并且对您来说这个“闭包”有什么区别没有意义,那么您可能可以忽略整个问题并继续前进更重要的事情。


1 有时,您使用哪种“等效”语法来编写代码确实会影响操作行为,例如性能。通常(但并非总是)这种影响可以忽略不计。作为初学者,我强烈建议暂时忽略这些问题,所以我没有提到它们。一旦您对所有语言元素的含义有了透彻的了解,就可以更轻松地学习推理代码如何执行所涉及的原则。

它有时也会影响 GHC 推断类型的方式(主要不是实际上它们是 lambda 的事实,但是如果您绑定函数名称而没有像 plusOne = \x -> x + 1 那样的语法参数,您可能会遇到单态限制,但是这是许多 Stack Overflow 问题中涵盖的另一个主题,因此我不会在此处解决。

2 在这种情况下,您还可以使用运算符部分将更简单的函数文字编写为 (+1)

3 现在我将教你关于闭包的知识,这样我就能解释为什么你不需要了解闭包。 :P

,

没有任何区别,除了一个:lambda 表达式不需要名称。出于这个原因,它们有时在其他语言中被称为“匿名函数”。

如果你打算经常使用一个函数,你需要给它一个名字,如果你只需要它一次,一个 lambda 通常会这样做,因为你可以在你使用它的地方定义它。>

当然,您可以在匿名函数诞生之后命名它

genIfEven2 = \f x -> isEven f x

那将完全等同于您的定义。