为什么函数组合有时需要两个“.”的结合两个功能

Why function composition sometimes requires two "." 's to combine two functions

所以这个问题很简单,但我似乎无法理解这个概念。

要编写普通函数,只需执行以下操作即可:

lowerNoSpaces = filter (/= ' ') . map toLower

但是,有时这会行不通:

myConcatMap = concat . map

它给出错误:

<interactive>:236:1: error:
    * Non type-variable argument
        in the constraint: Foldable ((->) [a1])
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        concattMap :: forall a1 a2.
                      Foldable ((->) [a1]) =>
                      (a1 -> a2) -> [a2]

但是当同样的函数这样表达时:

myConcatMap = (concat .) . map

它完全符合预期。

我知道这是有原因的,但我已经盯着它看了一段时间,仍然不太明白为什么原来的不行,而这个不行。

为什么有两个“.”的?

组合运算符(.)的类型是(a->b) -> (b->c) -> (a->c),这意味着它接受2个一元函数并将前者的输出转发给后者。

concat . map 的情况下,map 函数是二进制的。它的类型 (a->b) -> [a] -> [b] 不适合 (.) 类型的 (b->c) 部分。

嗯,它确实如此:map(a->b) 参数进入 (b->c)b 并且 [a] -> [b] "leftover" 进入 c,但这会导致类型检查器认为您实际上有一个函数列表并想对其进行操作。这实际上是可能的,但是存在与您最初的问题无关的类型问题,这显然不是您想要做的。

您的代码可以这样重写:

myConcatMap f = concat . map f

现在我们已经用 f 探测了 (a->b) 参数,它变成了一个一元函数,组合得很好。

这是因为 map 是一个双参数函数,而您想在提供两个参数后才应用 concat。请记住,Haskell 多参数​​函数是柯里化的,即它实际上是

map :: (a->b) -> ([a]->[b])

因此,如果你写一个组合 c . mapc 的参数必须是 [a]->[b] 类型的东西。但是 concat 的参数应该是一个列表,即 [b] 或实际上 [[e]].

类型的东西

解决方案:

  • 显式传递第一个参数。

    myConcatMap f = concat . map f
    

    这是可行的,因为 map f 只是一个单参数函数 [a] -> [b],因此您可以在它前面组合 concat

  • 在函数 前面写 concat,这是将 map 应用于其第一个参数 的结果。这就是您在示例中所做的。

让我们看看一些类型签名。

concat :: Foldable t => t [a] -> [a]
map :: (a -> b) -> [a] -> [b]
(.) :: (b -> c) -> (a -> b) -> a -> c

现在,concat . map 有意义吗?为简单起见,我们假设 Foldable 成员只是列表。

(.) 的第一个参数是 concat,类型为 [[d]] -> [d](以避免名称冲突)。将其替换为 (b -> c) 得到:

(.) concat :: (a -> [[d]]) -> a -> [d]

尝试将其应用于 map。应用于单个参数,map 给你一个函数;这与 (.) concat 期望的第一个参数的 [[d]] 不匹配。我们遇到了问题。

但是,如果您先向 map 提供一个参数会怎么样? map g 有签名 [e] -> [f],所以我们最终得到这个类型签名:

(.) concat (map g) :: [e] -> f

那是类型检查,所以我们这里有一些有意义的东西!如果您注意到,我们首先将 map 应用于 g,然后将 (.) concat(相当于 (concat .))应用于该结果,以便可以像这样重写该函数:

(concat .) . map $ g

这种形式允许我们完全摆脱 g 并将您的函数 myConcatMap 放入无点形式:

myConcatMap = (concat .) . map

这很容易从 (.) 的定义和 Haskell 语法的知识中推导出来。

您从 myConcatMap 的更明确的定义开始,即

\f -> \xs -> concat (map f xs)

根据组合运算符的定义,你可以这样写

\f -> concat . (map f)

在前缀位置使用 . 而不是作为中缀运算符重写它。

\f -> (.) concat (map f)

并添加一些多余的括号,因为函数应用程序是左结合的。

\f -> ((.) concat) (map f)

使用节语法重写此代码,使 . 再次成为中缀运算符

\f -> (concat .) (map f)

并再次应用 (.) 的定义,使用函数 (concat .)map:

(concat .) . map