Haskell:箭头优先级与函数参数

4

我是一名有一定经验的Haskell程序员,但只有几个小时的经验,所以答案可能很明显。

在观看了A taste of Haskell之后,当Simon解释append(++)函数的工作原理及其参数时,我感到很迷惑。

因此,在这里,他讲到了这一部分。

首先,他说(++) :: [a] -> [a] -> [a]可以理解为一个函数,它获取两个列表作为参数,并返回一个列表(在最后一个箭头之后)。然而,他补充说,实际上发生的事情是:(++) :: [a] -> ([a] -> [a]),该函数仅接受一个参数并返回一个函数。

我不确定如何理解返回的函数闭包如何获得它期望的第一个列表作为参数。

在演示文稿的下一张幻灯片中,我们有以下实现:

(++) :: [a] -> [a] -> [a]

[]     ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

如果我认为(++)接收两个参数并返回一个列表,那么这段代码加上递归就足够清晰了。
如果我们考虑到(++)只接收一个参数并返回一个列表,那么ys从哪里来?返回的函数在哪里?

1
查找 _柯里化_:在大多数函数式编程语言中,不存在接受多个参数的函数。"二元函数"是一个技术上不正确的术语,用于表示接受一个参数("第一个")并返回一个函数的函数。返回的函数接受一个参数("第二个"),最终返回结果。例如,(++) [1,2] 是一个可以应用于 [4,5] 以生成 [1,2,4,5] 的函数。 - chi
3个回答

4
理解这一点的诀窍在于,所有的 Haskell 函数最多只接受一个参数,只是类型签名和语法糖中的隐式括号使其看起来像有更多的参数。以 ++ 为例,以下转换都是等价的。
xs ++ ys = ...
(++) xs ys = ...
(++) xs = \ys -> ...
(++) = \xs -> (\ys -> ...)
(++) = \xs ys -> ...

另一个快速的例子:
doubleList :: [Int] -> [Int]
doubleList = map (*2)

这里我们有一个带有一个参数doubleList的函数,没有任何显式参数。与其等效的写法是:

doubleList x = map (*2) x

或者以下任何一个。
doubleList = \x -> map (*2) x
doubleList = \x -> map (\y -> y * 2) x
doubleList x = map (\y -> y * 2) x
doubleList = map (\y -> y * 2)

“doubleList” 的第一个定义采用通常所称的无点符号表示法进行编写,它被这样称呼是因为在支持它的数学理论中,参数被称为“点”,因此无点符号即为“没有参数”的意思。

以下是一个更复杂的例子:

func = \x y z -> x * y + z
func = \x -> \y z -> x * y + z
func x = \y z -> x * y + z
func x = \y -> \z -> x * y + z
func x y = \z -> x * y + z
func x y z = x * y + z

现在如果我们想要完全删除对参数的所有引用,我们可以利用点( . )运算符执行函数组合:
func x y z = (+) (x * y) z    -- Make the + prefix
func x y = (+) (x * y)        -- Now z becomes implicit
func x y = (+) ((*) x y)      -- Make the * prefix
func x y = ((+) . ((*) x)) y  -- Rewrite using composition
func x = (+) . ((*) x)        -- Now y becomes implicit
func x = (.) (+) ((*) x)      -- Make the . prefix
func x = ((.) (+)) ((*) x)    -- Make implicit parens explicit
func x = (((.) (+)) . (*)) x  -- Rewrite using composition
func = ((.) (+)) . (*)        -- Now x becomes implicit
func = (.) ((.) (+)) (*)      -- Make the . prefix

如您所见,有很多不同的方法来编写某个函数,有些具有显式“参数”,非常易读(例如func x y z = x * y + z),而有些则只是一堆符号的混乱表示,没有什么意义(例如func = (.) ((.) (+)) (*))。


好的,我可以将函数视为lambda函数的别名。参数不是传递而是根据需要进行填充/替换,例如:add = \x y -> x + yadd 3 4 <=> (\x y -> x + y) 3 4 - D.Naesuko
然而,我读到点无风格是最受欢迎的一种。有没有办法用高阶函数来编写你的最后一个 func = 行? - D.Naesuko
2
@D.Naesuko 点无风格编程仅在看起来更清晰时才被推荐使用。只有当您删除一个函数参数时,才有时会出现这种情况,而当您删除多个参数时,这种情况很少发生。 - Ørjan Johansen

2
你对函数柯里化的工作原理感到困惑。
考虑以下(++)函数定义。
接受两个参数,生成一个列表:
(++) :: [a] -> [a] -> [a]
[]     ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

接受一个参数,生成一个函数,该函数接受一个列表并生成一个列表:
(++) :: [a] -> ([a] -> [a])
(++) []     = id
(++) (x:xs) = (x :) . (xs ++)

如果你仔细观察,这些函数将始终产生相同的输出。通过删除第二个参数,我们已经将返回类型从[a]更改为[a] -> [a]
  • 如果我们向(++)提供两个参数,我们将得到一个类型为[a]的结果
  • 如果我们只提供一个参数,我们将得到一个类型为[a] -> [a]的结果
这被称为函数柯里化。我们不需要为具有多个参数的函数提供所有参数。如果我们提供少于总参数数的参数,而不是获得“具体”结果([a]),我们将得到一个函数作为结果,该函数可以接受剩余的参数([a] -> [a])。

2
也许这会有所帮助。首先,让我们不使用可能令人困惑的运算符表示法来书写它。
append :: [a] -> [a] -> [a]
append [] ys = ys
append (x:xs) ys = x : append xs ys

我们可以逐个应用参数:
appendEmpty :: [a] -> [a]
appendEmpty = append []

我们可以等价地写成:
appendEmpty ys = ys

从第一个方程式中可得:

如果我们使用非空的第一个参数:

-- Since 1 is an Int, the type gets specialized.
appendOne :: [Int] -> [Int]
appendOne = append (1:[])

我们可以等价地写成:
appendOne ys = 1 : append [] ys

从第二个方程开始。


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