Haskell中的类型签名和函数方程

12

我是Haskell和函数式编程的新手。我正在阅读《Real World Haskell》,但我发现我对一些示例感到困惑。

具体来说,在第9章的“用于谓词的特定领域语言”一节中,有带有w x y z参数的示例。

我把它简化成了这样:

为什么这段代码可以编译?

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

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

根据类型签名,f 明显接受一个参数并返回一个函数。但是,我可以编写函数方程使其接受两个参数并返回一个 int。为什么会这样?这是否意味着类型签名被忽略了?
这是柯里化吗?这是某种闭包吗? 如果我正确理解了 http://www.haskell.org/haskellwiki/Currying,那么它似乎与那里定义的柯里化相反 - 我的 f 函数正在接受多个参数而不是单个参数!
此外,任何回答的人都可以提供一个指向某种 Haskell 文档的链接,其中说明了这种能力(如果可能的话)。
编辑:
思考了一段时间之后,你们两个似乎在暗示:
1)这种语法是语法糖,f 总是只有一个参数,无论方程中写了多少个参数
2)应用 f 时,函数体将(总是?)转换为存根(实际上是返回的函数),其中 x 固定为给定的参数(4),y 是一个参数。
3)然后将这个新函数应用于 5,替换 y,然后评估 + 函数。
我真正感兴趣的是,在哪里可以找到类似“在函数方程中,如果你写了多个参数,它实际上是语法糖,并且会发生以下情况...”这样的内容。或者这对每个人都是如此显而易见,除了我?
编辑 II:

真正的启发性答案在下面的@luqui评论中,不幸的是,我不认为我可以将评论标记为答案。

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

对我来说,每个人都在下面说的其他一切都是由此而来的。

我在Haskell入门指南中找到了这方面的资料,链接如下:http://haskell.cs.yale.edu/tutorial/functions.html,位于第3.1节,称为Lambda抽象。

事实上,等式:

inc x = x+1 add x y = x+y

实际上是简写为:

inc = \x -> x+1 add = \x y -> x+y

虽然它没有使用“语法糖”这个词组,而是使用了更加数学化的“简写”这个词,但作为一个程序员,我读到这里时就会想到“糖” :-)


2
确实不明显,但这是我们必须迅速适应并理解 Haskell 的一件事情。也许这就是为什么作者们忘记明确提到它的原因。我相信《Learn You a Haskell》中肯定有描述。 - luqui
3
你的第二点有一些偏差。一旦你定义了函数,它接受它所接受的参数。当你调用它时,如f 4 5,这相当于解析为(f 4) 5,因此f将以4作为参数被调用并返回\y -> 4 + y的函数。然后你有(\y -> 4 + y) 5,它变成了4 + 5,然后是9。诀窍在于函数类型和函数定义是右结合的,而函数应用是左结合的,因此它们可以很好地对齐。 - Antal Spector-Zabusky
@Antal s-z 感谢您纠正我并指出函数类型和函数应用之间的对称性。不过您也可以说“您的函数实际上并没有接受任何参数,而是所有的lambda都被柯里化了”。 - Or When
为什么这个问题没有被接受的答案? - SwiftsNamesake
5个回答

10
这是柯里化。如类型签名所示,f 只接受一个参数。这个参数为4,并返回一个函数,该函数立即应用于5。实际上,这两个类型签名:Int -> Int -> IntInt -> (Int -> Int) 在 Haskell 中是等效的。
编辑:在您提供的页面中发现的 Partial Application 链接解释了这一点。
编辑2:您问 Haskell 的柯里化行为在哪里定义。我不知道这是否是您要找的内容:Haskell 98 Revised Report 中的第 3.3 Curried Applications and Lambda Abstractions 节说:
“函数应用”写作e1 e2。应用向左结合,因此可以省略(f x) y 中的括号。

我仍然不完全理解。 我知道Int -> (Int -> Int)和Int -> Int -> Int是等价的,但说实话这也很令人困惑,我并没有完全理解这个概念。 在实际应用中意味着什么? 请注意,我询问的是类型方程而不是类型签名。你给出的链接具有我熟悉的部分应用形式,它将一个看似“正常”的具有两个参数的函数与在不同函数中固定第一个参数。 但在我的情况下,它是一个单一的函数。 经过思考,我有了一个想法,我会写在下面。 - Or When
通过“类型方程”,我认为你指的是“函数方程”。我添加了一个链接到Haskell 98 Revised Report,希望这有助于澄清问题。函数应用是左结合的,所以f x y与(f x) y是相同的,f x y z与((f x) y) z也是相同的,依此类推。Haskell报告将此称为“柯里化应用”。 - carnieri

4
-> 运算符是右结合的,即 t -> t -> t 等同于 t -> (t -> t)
如果您重新编写
f x y = x+y

以等效形式
f x = \y -> x + y

这个连接应该变得明显。

但是,为什么这是等价的形式呢? - Or When
2
@Or When,这只是符号表示。f x y z = blahf = \x -> \y -> \z -> blah 的简写。 - luqui
请参阅Haskell 2010标准的4.4.3.1节,以了解用于支持模式匹配的确切展开方式。 - Toxaris

2
这绝对是柯里化的一部分。虽然我无法立即找到文档中明确说明这种行为的位置,但我们只需要稍微检查一下有关柯里化的数学知识就可以了。
正如我们所知,具有签名 Int ->(Int -> Int) 的函数接受一个 Int,并返回一个接受 Int 并返回一个 Int函数。我们可以提供它所需的两个Int来得到最终的Int,就像你的例子中一样。
f :: Int -> (Int -> Int)
f x y = x+y

如果我们只提供第一个参数,我们将得到一个需要另一个参数的函数。这就是柯里化的核心。
简单来说,柯里化是right-associative的。换句话说,Int -> (Int -> Int)Int->Int->Int是相同的,只不过我们添加了括号以使其更明显:
f 3

不是缺少一个参数,而是实际上返回了一个类型为 Int->Int 的函数。

这有点像在数学上学习加法的结合律时一样。

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

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

让我困惑的不是函数application,我对它很熟悉,而是函数equation。我只需要看到实现细节,f x y = .... 被翻译为 f = \x -> \y -> ....(在我看来这确实是语法糖)。 - Or When
@Or When:啊,好的,我误解了 :-p - Zach L

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。

“我们可以只提供这两个整数…” 这部分听起来有点像魔法。但是,这一点如何与 Haskell 函数仅接受一个参数的事实相一致呢?展示实际的“实现”,即 f x y = ... 的实际定义应该是 f = \x -> \y -> ...,这样会更有帮助。 - Or When
笔误:应该是 h 6 -- 13 而不是 g 6。 - Adam Gordon Bell

2

经过进一步的思考,我认为完整的解释应该是:

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

f x y = x + y

被视为
(1) f = \x -> \y -> x + y

这种处理方法同样适用于lambda函数: \x y -> 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的实现(“等式”)。如果指定了一个或两个参数,则编译器将生成必要的lambda以符合形式 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'' :: (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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接