【问题标题】:Haskell: arrow precedence with function argumentsHaskell:函数参数的箭头优先级
【发布时间】:2015-11-15 21:39:49
【问题描述】:

我是一个相对经验丰富的 Haskell 程序员,有几个小时的经验,所以答案可能很明显。

看完《A taste of Haskell》后,当 Simon 解释 append (++) 函数真正如何与它的参数一起工作时,我迷失了。

所以,here's the part where he talks about this

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

我不确定返回的函数闭包如何获得第一个列表,因为它也需要一个参数。

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

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

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

如果我认为 (++) 接收两个参数并返回一个列表,那么这段代码连同递归就足够清晰了。

如果我们认为(++) 只接收一个参数并返回一个列表,ys从哪里来?返回的函数在哪里?

【问题讨论】:

  • 查找currying:在大多数函数式编程语言中,没有函数接受多个参数这样的东西。 “二元函数”是一个技术上不正确的术语,它指的是一个函数采用 one 参数(“第一个”参数)并返回一个函数。这个返回的函数接受 一个 参数(“第二个”),最后返回结果。例如。 (++) [1,2] 是一个函数,可以应用于[4,5] 以产生[1,2,4,5]

标签: function haskell recursion arguments operator-precedence


【解决方案1】:

您对 Function Currying 的工作方式感到困惑。

考虑(++)的以下函数定义。

接受两个参数,生成一个列表:

(++) :: [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】:

    理解这一点的诀窍是所有haskell函数最多只接受1个参数,只是类型签名和语法糖中的隐式括号使它看起来好像有更多参数。以++为例,下面的变换都是等价的

    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 函数的 aliases。参数没有通过,而是按照以下方式填充/替换add = \x y -> x + y, add 3 4 (\x y -> x + y) 3 4
    • 然而,我读到无点样式是最受欢迎的样式。有什么办法可以用高阶函数编写你的最后一行func = 吗?
    • @D.Naesuko 无点样式只有在看起来更清晰时才是首选。仅当您删除 一个 函数参数时会出现这种情况,而当您删除多个函数参数时,很少会出现这种情况。
    【解决方案3】:

    也许这会有所帮助。首先让我们不使用可能会造成混淆的运算符符号来编写它。

    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
    

    从第二个等式。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-31
      • 2011-12-29
      • 1970-01-01
      • 2014-06-26
      • 1970-01-01
      • 2021-08-10
      • 2017-05-21
      • 2017-10-02
      相关资源
      最近更新 更多