为何Haskell函数组合有时需用两个"."组合函数?
为什么
concat . map报错,而(concat .) . map正常工作? 这个问题其实是Haskell函数组合中类型匹配的经典坑,我们一步步拆解就清楚了。
先回顾函数组合的核心规则
函数组合运算符(.)的定义和类型是:
(.) :: (b -> c) -> (a -> b) -> a -> c
它的核心要求是:第一个函数的输入类型,必须等于第二个函数的输出类型,这样才能把两个函数“串”起来,形成f(g x)的效果。
先看你能理解的正常例子:filter (/= ' ') . map toLower
我们拆解类型:
map toLower的类型:String -> String(即[Char] -> [Char]),输入字符串,输出转小写后的字符串。filter (/= ' ')的类型:String -> String,输入字符串,输出去掉空格后的字符串。
第二个函数的输出String完全匹配第一个函数的输入String,所以组合毫无问题,直接生效。
为什么concat . map会报错?
先明确两个函数的类型:
map的类型:(a -> b) -> [a] -> [b]——这是个高阶函数,接受一个元素转换函数,返回另一个函数(从列表到列表的函数)。concat的类型:Foldable t => t [c] -> [c]——它接受一个可折叠容器(比如最常见的[[c]],即列表的列表),把容器里的所有子列表拼接成一个列表。
当你写concat . map时,相当于定义了\x -> concat (map x)。此时map x的输出是[a] -> [b](一个函数),而concat需要的输入是Foldable t => t [c](可折叠容器)。
Haskell会尝试把[a] -> [b]这个函数类型匹配到t [c]上:它会把t推断为(->) [a](函数类型的柯里化表示),同时要求Foldable ((->) [a])这个约束。但默认情况下Haskell根本没有为函数类型实现Foldable实例,所以就出现了你看到的错误:既需要开启FlexibleContexts扩展,而且就算开了也没用——因为没有对应的实例支持这种操作。
为什么(concat .) . map能正常工作?
我们拆解这个嵌套组合的逻辑:
- 先看
(concat .):这是把(.)部分应用到concat上,得到的函数类型是(x -> [[c]]) -> x -> [c]。它的作用是:接受一个“返回列表的列表”的函数,然后把这个函数的输出传给concat,最终返回拼接后的列表。 - 再把这个函数和
map组合:此时我们把map的类型调整为(a -> [c]) -> [a] -> [[c]](把b换成[c],因为concat需要的是列表的列表)。map的输出是[a] -> [[c]],正好匹配(concat .)的输入类型x -> [[c]](这里x就是[a])。 - 组合后的效果:
(concat .) . map等价于\f -> concat . map f——也就是先对列表的每个元素应用f(得到列表的列表),再把所有子列表拼接起来,这正好就是标准库中concatMap的定义!
一句话总结
concat . map试图直接把“接受容器的concat”和“返回函数的map”组合,类型完全不匹配,因为map的输出是函数,不是concat能处理的可折叠容器。(concat .) . map则是先把concat和“生成列表的列表”的逻辑包装起来,再和map组合,让map先产出concat需要的输入,类型完美匹配,因此能正常工作。
内容的提问来源于stack exchange,提问作者BryceTheGrand
相关产品推荐
相关产品推荐

