Haskell:“groupBy”令人惊讶的行为

时间:2009-08-22 16:27:24

标签: haskell combinators

我试图弄清楚库函数groupBy(来自Data.List)的行为,它声称通过作为第一个参数传入的“相等测试”函数对列表的元素进行分组。类型签名表明,相等性测试只需要具有类型

(a -> a -> Bool)

然而,当我在GHCi 6.6中使用(<)作为“相等测试”时,结果并不是我所期望的:

ghci> groupBy (<) [1, 2, 3, 2, 4, 1, 5, 9]
[[1,2,3,2,4],[1,5,9]]

相反,我希望运行严格增加的数字,如下:

[[1,2,3],[2,4],[1,5,9]]

我错过了什么?

4 个答案:

答案 0 :(得分:34)

查看groupBy ghc 实现:

groupBy                 :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy _  []           =  []
groupBy eq (x:xs)       =  (x:ys) : groupBy eq zs
                           where (ys,zs) = span (eq x) xs

现在比较这两个输出:

Prelude List> groupBy (<) [1, 2, 3, 2, 4, 1, 5, 9]
[[1,2,3,2,4],[1,5,9]]
Prelude List> groupBy (<) [8, 2, 3, 2, 4, 1, 5, 9]
[[8],[2,3],[2,4],[1,5,9]]

简而言之,会发生什么:groupBy假设给定函数(第一个参数)测试相等性,因此假设比较函数是 reflexive 传递对称(见equivalence relation)。这里的问题是 less-than 关系不是自反的,也不是对称的。


编辑:以下实施仅假定传递性:

groupBy' :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy' _   []                        = []
groupBy' _   [x]                       = [[x]]
groupBy' cmp (x:xs@(x':_)) | cmp x x'  = (x:y):ys
                           | otherwise = [x]:r
  where r@(y:ys) = groupBy' cmp xs

答案 1 :(得分:9)

“&lt;”的事实不是一个平等的考验。

你可能会期待一些行为,因为你以不同的方式实现它,但这不是它所承诺的。

为什么输出它是一个合理的答案的例子是如果它扫过它,做

[1, 2, 3, 2, 4, 1, 5, 9] ->
[[1,2,3], [2,4], [1,5,9]]

现在有3组相等的元素。所以它检查它们中的任何一个是否实际上是相同的:

因为它知道每个组中的所有元素是相等的,所以它可以只查看每个元素中的第一个元素,1,2和1。

1&gt; 2?是!所以它合并了前两组。

1&gt; 1?没有!所以它留下了最后一组。

现在它将所有元素进行了比较。

......只是,你没有传递它预期的那种功能。

简而言之,when it wants an equality test, give it an equality test

答案 2 :(得分:9)

问题是Haskell Report中groupBy的引用实现将元素与第一个元素进行了比较,因此这些组并没有严格增加(它们只需要比第一个元素都大)。您想要的是groupBy的一个版本,用于测试相邻的元素,例如实现here

答案 3 :(得分:0)

我想指出,groupBy函数还要求您的列表在应用之前先进行排序。

例如:

equalityOp :: (a, b1) -> (a, b2) -> Bool
equalityOp x y = fst x == fst y

testData = [(1, 2), (1, 4), (2, 3)]

correctAnswer = groupBy equalityOp testData == [[(1, 2), (1, 4)], [(2, 3)]]

otherTestData = [(1, 2), (2, 3), (1, 4)]

incorrectAnswer = groupBy equalityOp otherTestData == [[(1, 2)], [(2, 3)], [(1, 4)]]

之所以会出现这种现象,是因为groupBy在其定义中使用了span。为了获得不依赖于我们以任何特定顺序拥有基础列表的合理行为,我们可以定义一个函数:

groupBy' :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy' eq []     = []
groupBy' eq (x:xs) = (x:similarResults) : (groupBy' eq differentResults)
    where similarResults   = filter (eq x) xs
          differentResults = filter (not . eq x) xs