12

我是 Haskell 和函数式编程的新手。我正在阅读 Real World Haskell,我意识到我对一些例子感到困惑。

具体来说,这是在第 9 章的“用于谓词的领域特定语言”一节中,具有 wxyz 参数的示例。

我把它归结为:

为什么这段代码会编译?

f :: Int -> (Int -> Int)
f x y = x+y

main = do
  let q = f 4 5
  putStr  (show (q))

根据类型签名,f显然是接受 1 个参数并返回一个函数。但是,似乎我可以编写函数方程,因此它将接受两个参数并返回一个 int。为什么这可能?这是否意味着类型签名被忽略?

这是咖喱吗?这是某种封闭吗?如果我正确理解了这个http://www.haskell.org/haskellwiki/Currying,那么它似乎与那里定义的 currying 有点相反——我的f函数采用多个参数而不是单个参数!

此外,任何人都可以回答请提供指向某种 Haskell 文档的链接,其中说明了这种能力(如果可能的话)。

编辑:

在考虑了一段时间之后,你们两个似乎暗示的是:

1) 这个语法是语法糖,f 永远只有一个参数,不管方程里写了多少个参数

2) 在应用 f 时,函数体将(总是?)被转换为一个存根(实际上是返回的函数),其中 x 固定为给定的参数 (4),y 是一个参数。

3) 然后将这个新函数应用于替换 y 的 5,然后计算 + 函数。

我真正感兴趣的是,它到底在哪里说“在函数方程中,如果你写了多个参数,它真的是语法糖,实际上会发生以下情况......”正如我在上面写的那样。或者这对除了我以外的所有人来说都那么明显吗?

编辑二:

真正令人大开眼界的答案在下面的@luqui 评论中,不幸的是,我认为我无法将评论标记为答案。

事实上 fxy = ... 实际上是语法糖: f = \x -> \y -> ...

对我来说,下面每个人所说的其他一切都来自于此。

我在 Haskell 的 Gentle Introduction to Haskell 中找到了一种来源:http: //haskell.cs.yale.edu/tutorial/functions.html在第 3.1 节中,称为 Lambda Abstractions。

事实上,方程:

公司 x = x+1 添加 xy = x+y

是真正的简写:

公司 = \x -> x+1 添加 = \xy -> x+y

虽然它不使用短语“语法糖”,但它使用了更多,呃,数学导向的单词“速记”,但作为一名程序员,我将其读为“糖”:-)

4

5 回答 5

10

It is currying. As the type signature says, f takes only one parameter. That would be 4. It then returns a function, which is immediately applied to 5. In fact, these two type signatures:

Int -> Int -> Int

and

Int -> (Int -> Int)

are equivalent in Haskell.

EDIT: this link about Partial Application, which I found inside the page you provided, explains this.

EDIT 2: You asked where the currying behavior of Haskell is defined. I don't know if this is what you're looking for: the Haskell 98 Revised Report, in section 3.3 Curried Applications and Lambda Abstractions, says:

Function application is written e1 e2. Application associates to the left, so the parentheses may be omitted in (f x) y.

于 2011-01-22T14:50:41.013 回答
4

->运算符是右结合的,即与t -> t -> t相同t -> (t -> t)

如果你重写

f x y = x+y

等价形式

f x = \y -> x + y

这个conncetion应该是显而易见的。

于 2011-01-22T16:01:42.660 回答
2

这绝对是有点咖喱。虽然我无法立即找到它在文档中明确说明这种行为的位置,但我们所要做的就是检查一下关于柯里化的数学。

众所周知,带有签名的函数Int ->(Int -> Int)接受一个Int,并返回一个接受一个并返回一个 Int的函数。Int我们可以只提供Int获得最终 int 所需的两个 s ,就像在您的示例中一样:

f :: Int -> (Int -> Int)
f x y = x+y

如果我们只提供第一个参数,我们会得到一个需要另一个参数的函数。咖喱的面包和黄油。

简单地说,柯里化是右联想。换句话说,Int -> (Int -> Int)与 相同Int->Int->Int,只是我们添加了括号以使其更明显:

f 3

没有缺少参数,但实际上返回了一个 type 的函数Int->Int

当您学习加法的关联属性时,这有点像回到数学中。

3 + 2 + 1 = (3 + 2) + 1 = 3 + (2 + 1)

无论我们如何放置括号,结果都是一样的。

于 2011-01-22T20:58:23.543 回答
2

它不是真正的语法糖,它只是函数应用程序在 Haskell 中的工作方式。

考虑:

f :: Int -> Int -> Int -> Int 
f x y z = x + y + z

g = f 4
h = g 4 5

f 4 4 5 -- 13
g 4 5   -- 13
g 6     -- 13

您可以在 ghci 中使用它来确认。g是函数的部分应用f——它的类型是g :: Int -> Int -> Int.

你也可以写:

((f 4) 4) 5  -- 13 

在这种情况下(f 4),返回一个接受两个附加参数((f 4) 4)的部分应用函数,返回一个接受一个参数的部分应用函数,整个表达式减少到 13。

于 2011-01-22T22:22:47.983 回答
2

在考虑了更多之后,我认为完整的解释应该是这样的:

Haskell 函数只能接受一个参数并返回一个参数。Haskell 允许我们假装传递了几个参数,但这种形式被视为一系列嵌套的 lambda 函数。

f x y = x + y

被视为

(1) f = \x -> \y -> x + y

这种处理也适用于 lambda 函数 \xy -> x + y 被视为 \x -> \y -> x + y

这允许我们将类型声明视为左关联,即: f :: Int -> Int -> Int 实际上是 f :: (Int -> (Int -> Int)) 完全符合上面的(1): f 没有参数,但返回一个接受 Int 的函数。该函数又返回一个接受另一个 Int 的函数,并返回一个 Int。

这意味着如果我们想从一个函数返回一个函数,我们不需要做任何特别的事情,因为那是 Haskell 的“默认”模式。

这也意味着给定类型声明 f :: Int -> Int -> Int 我们可以用 0、1 或 2 个参数编写 f 的实现(“方程式”)。如果指定了一个或两个参数,编译器将生成必要的 lambdas 以符合格式 f :: (Int -> (Int -> Int))

f = \x -> \y -> x + y

f x = \y -> x + y  -- \x -> is generated

f x y = x + y -- \x -> \y is generated

但是在每种情况下,看起来接受两个参数的函数应用程序都会成功编译,因为它总是会被转换为第一种形式,例如

f 4 5 --> (\x -> (\y -> x + y) 5 ) 4

内部函数应用程序将返回柯里化形式 (x + 5)

这启用了偏函数应用,我们可以只给 f 一个参数并返回一个偏函数。

此外,更改函数类型的优先级:

f'' :: (Int -> Int) -> Int

改变了含义——我们接受一个函数获取一个 Int 并返回一个作为单个参数,并返回一个 Int。
假设我们已经在某个地方定义了这个函数,然后用整数参数调用这个函数,例如

f'' 4 5

不会编译。

编辑:

此外,即使最后一个参数在括号中,或者是类型声明,这也成立。

例如,在下文中,最后一对括号是可选的,因为如果它们不存在,编译器无论如何都会将它们放入“lambda'd”形式。

t4 :: (Int -> Int) -> (Int -> Int)
t4 f i = f i + i

可以这样应用:

t4 (\x -> x*10) 5

此外,鉴于:

type MyIntInt = Int -> Int

然后:

t5 :: MyIntInt -> MyIntInt

等效于 t4,但令人困惑,因为 MyIntInt 的含义在两个地方都不同。第一个是第一个参数的类型。
第二个被“扩展”为 Int -> Int (可能是因为运算符的右结合性),这意味着 t5 在函数方程和函数应用程序中看起来可以接受 0 到 2 个参数(而实际上总是接受 0并返回嵌入式 lambda,正如我在上面详述的那样)。

例如,我们可以像 t4 一样写 t5:

t5 f i = f i + i
于 2011-01-23T10:32:41.800 回答