【问题标题】:efficiently checking that all the elements of a (big) list are the same有效地检查(大)列表的所有元素是否相同
【发布时间】:2011-09-01 13:13:33
【问题描述】:

问题

假设我们有一个列表xs(可能是一个很大的列表),并且我们想要检查它的所有元素是否相同。

我想出了各种想法:

解决方案 0

检查tail xs 中的所有元素是否等于head xs

allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)

解决方案 1

检查length xs 是否等于通过从xs 获取元素而获得的列表的长度,同时它们等于head xs

allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)

解决方案 2

递归解决方案:如果xs 的前两个元素相等,则allTheSame 返回TrueallTheSamexs 的其余部分返回True

allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
    where  n = length xs

解决方案 3

分而治之:

allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
  | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
    where n = length xs
          split = splitAt (n `div` 2) xs

解决方案 4

我只是在写这个问题时想到了这个:

allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)

问题

  1. 我认为解决方案 0 的效率不是很高,至少在内存方面是这样,因为map 将在将and 应用于其元素之前构造另一个列表。我说的对吗?

  2. 解决方案 1 仍然不是很有效,至少在内存方面,因为takeWhile 将再次构建一个额外的列表。我说的对吗?

  3. 解决方案 2 是尾递归的(对吗?),它应该非常有效,因为一旦 (xs !! 0 == xs !! 1) 为 False,它就会返回 False。我说的对吗?

  4. 解决方案 3 应该是最好的,因为它的复杂度应该是 O(log n)

  5. 解决方案 4 在我看来很像 Haskellish(是吗?),但它可能与解决方案 0 相同,因为 all p = and . map p(来自 Prelude.hs)。我说的对吗?

  6. 还有其他更好的写allTheSame的方法吗?现在,我希望有人会回答这个问题,告诉我有一个内置函数可以做到这一点:我用 hoogle 搜索过,但没有找到。无论如何,由于我正在学习 Haskell,我相信这对我来说是一个很好的练习:)

欢迎任何其他评论。谢谢!

【问题讨论】:

  • 您似乎没有意识到您正在执行的某些操作的复杂性:lengthO(n),因此您应该更喜欢列表中的模式匹配而不是长度。此外应该注意的是,索引i 处的列表的索引是O(i),而将列表一分为二同样是O(n)。 (如果所有 O(1) 的位置,您的分而治之的解决方案仍然没有对数运行时间 - 它将是 O(n log n))。
  • @sepp2k O(n*log(n)) 是它当前的复杂性。给定有效的lengthsplitAt,它将在O(n) 中运行。然而,这是错误的。我认为它会为[1,1,2,2] 返回True
  • 注意and . mapall的定义,使得0和4等价。 Source
  • @sepp2k:好点!我应该使用模式匹配。但是我不明白你最后的话。鉴于splitAt 是 O(n),那么我知道我的解决方案 3 是 O(n lg n)(树的深度是 lg n,并且在每一级拆分都是 O(n))。但是,即使splitAt 是 O(1),你为什么说它是 O(n lg n)?
  • 对于解决方案 0,您可能希望使用模式匹配而不是 headtail

标签: list haskell


【解决方案1】:

Q1 -- 是的,我认为您的简单解决方案很好,没有内存泄漏。 Q4 - 解决方案 3 不是 log(n),通过一个非常简单的参数,您需要查看所有列表元素以确定它们是否相同,并且查看 1 个元素需要 1 个时间步。 Q5——是的。 Q6,见下文。

解决方法是输入并运行它

main = do
    print $ allTheSame (replicate 100000000 1)

然后运行ghc -O3 -optc-O3 --make Main.hs && time ./Main。我最喜欢最后一个解决方案(你也可以使用模式匹配来稍微清理一下),

allTheSame (x:xs) = all (==x) xs

打开 ghci 并在这些东西上运行 ":step fcn"。它将教你很多关于惰性评估正在扩展的内容。通常,当您匹配构造函数时,例如“x:xs”,这是恒定的时间。当您调用“长度”时,Haskell 需要计算列表中的所有元素(尽管它们的值仍然是“待计算”),因此解决方案 1 和 2 不好。

编辑 1

对不起,如果我之前的回答有点肤浅。似乎手动扩展内容确实有点帮助(尽管与其他选项相比,这是一个微不足道的改进),

{-# LANGUAGE BangPatterns #-}
allTheSame [] = True
allTheSame ((!x):xs) = go x xs where
    go !x [] = True
    go !x (!y:ys) = (x == y) && (go x ys)

似乎 ghc 已经对函数进行了专门化,但您也可以查看specialize pragma,以防它不适用于您的代码 [link]。

【讨论】:

  • 插入严格匹配会改变原函数的语义。
  • 感谢所有伟大的 cmets!我非常喜欢您使用模式匹配清理我的最后一个解决方案的建议。也感谢您的建议 :step ... 但我仍然需要了解 gchi 调试器:您能推荐一个好的教程吗?
  • 我并没有真正使用教程,只是启动它(ghci)并戳它,例如"let f = ..."、"1+2" 等。然后,您可以使用 ":l filename" 加载文件或使用 "ghci Filename.hs" 启动 ghci。另外,我使用 ":r" 不断地重新加载文件。输入 :help 获取命令列表——你应该看到 ":step single-step into "。干杯。
【解决方案2】:

gatoatigrado 的回答为衡量各种解决方案的性能提供了一些很好的建议。这是一个更具象征意义的答案。

我认为解决方案 0(或者,完全等效,解决方案 4)将是最快的。请记住,Haskell 惰性,所以map 不必在应用and 之前构造整个列表。建立对此的直觉的一个好方法是玩无穷大。比如:

ghci> and $ map (< 1000) [1..]
False

这会询问是否所有数字都小于 1,000。如果map 在应用and 之前构建了整个列表,那么这个问题将永远无法回答。即使您给列表一个非常大的右端点,该表达式仍然会快速响应(也就是说,Haskell 不会根据列表是否无限做任何“魔术”)。

为了开始我的示例,让我们使用以下定义:

and [] = True
and (x:xs) = x && and xs

map f [] = []
map f (x:xs) = f x : map f xs

True && x = x
False && x = False

这是allTheSame [7,7,7,7,8,7,7,7] 的评估顺序。会有额外的分享,写起来太痛苦了。为了简洁起见,我还将更早地评估 head 表达式(无论如何它都会被评估,所以几乎没有什么不同)。

allTheSame [7,7,7,7,8,7,7,7]
allTheSame (7:7:7:7:8:7:7:7:[])
and $ map (== head (7:7:7:7:8:7:7:7:[])) (tail (7:7:7:7:8:7:7:7:[]))
and $ map (== 7)  (tail (7:7:7:7:8:7:7:7:[]))
and $ map (== 7)          (7:7:7:8:7:7:7:[])
and $ (== 7) 7 : map (== 7) (7:7:8:7:7:7:[])
(== 7) 7 && and (map (== 7) (7:7:8:7:7:7:[]))
True     && and (map (== 7) (7:7:8:7:7:7:[]))
            and (map (== 7) (7:7:8:7:7:7:[]))
(== 7) 7 && and (map (== 7)   (7:8:7:7:7:[]))
True     && and (map (== 7)   (7:8:7:7:7:[]))
            and (map (== 7)   (7:8:7:7:7:[]))
(== 7) 7 && and (map (== 7)     (8:7:7:7:[]))
True     && and (map (== 7)     (8:7:7:7:[]))
            and (map (== 7)     (8:7:7:7:[]))
(== 7) 8 && and (map (== 7)       (7:7:7:[]))
False    && and (map (== 7)       (7:7:7:[]))
False

看看我们是如何不用检查最后 3 个 7 的?这是一种惰性求值,使列表更像一个循环。您的所有其他解决方案都使用昂贵的函数,例如 length(必须一直走到列表的末尾才能给出答案),因此它们的效率会降低,而且它们也不会在无限列表上工作。处理无限列表和高效通常在 Haskell 中并存。

【讨论】:

  • 很高兴你写了这个答案。在我完成我的工作后,我认为手动评估会很有用,但不想变得更长。虽然我认为在无限列表上使用allTheSame 的任何变体都不明智......
  • 是的,它只会在答案为False 时终止;-)。
  • 太棒了!感谢您花时间编写示例:这对我了解惰性评估的工作原理非常有帮助,我必须牢记这一点以备不时之需 :)
  • 即使它是 python 并且它是一个常规数组(python 容易混淆地称其为列表),我什至不会考虑分而治之。
【解决方案3】:

首先,我认为您不想使用列表。您的许多算法都依赖于计算长度,这很糟糕。您可能需要考虑 vector 包,与列表的 O(n) 相比,它会给您 O(1) 的长度。向量的内存效率也更高,尤其是当您可以使用 Unboxed 或 Storable 变体时。

话虽如此,您确实需要在代码中考虑遍历和使用模式。如果 Haskell 的列表可以按需生成并使用一次,则非常有效。这意味着您不应保留对列表的引用。像这样的:

average xs = sum xs / length xs

要求将整个列表保留在内存中(通过sumlength)直到两个遍历都完成。如果你可以一步完成你的列表遍历,那效率会高很多。

当然,您可能仍然需要保留列表,例如检查所有元素是否相等,如果不相等,则对数据执行其他操作。在这种情况下,对于任何大小的列表,您可能最好使用更紧凑的数据结构(例如向量)。

现在这已经不成问题了,下面来看看这些函数中的每一个。在我展示核心的地方,它是用ghc-7.0.3 -O -ddump-simpl 生成的。此外,当使用 -O0 编译时,不要费心判断 Haskell 代码的性能。使用您实际用于生产代码的标志编译它,通常至少 -O 和其他选项。

解决方案 0

allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)

GHC 生产这个核心:

Test.allTheSame
  :: forall a_abG. GHC.Classes.Eq a_abG => [a_abG] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 16 0}]
Test.allTheSame =
  \ (@ a_awM)
    ($dEq_awN :: GHC.Classes.Eq a_awM)
    (xs_abH :: [a_awM]) ->
    case xs_abH of _ {
      [] ->
        GHC.List.tail1
        `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
      : ds1_axJ xs1_axK ->
        letrec {
          go_sDv [Occ=LoopBreaker] :: [a_awM] -> GHC.Bool.Bool
          [LclId, Arity=1, Str=DmdType S]
          go_sDv =
            \ (ds_azk :: [a_awM]) ->
              case ds_azk of _ {
                [] -> GHC.Bool.True;
                : y_azp ys_azq ->
                  case GHC.Classes.== @ a_awM $dEq_awN y_azp ds1_axJ of _ {
                    GHC.Bool.False -> GHC.Bool.False; GHC.Bool.True -> go_sDv ys_azq
                  }
              }; } in
        go_sDv xs1_axK
    }

实际上,这看起来很不错。它会产生一个空列表的错误,但这很容易解决。这是case xs_abH of _ { [] -&gt;。在这个 GHC 执行了 worker/wrapper 转换之后,递归的 worker 函数就是 letrec { go_sDv 绑定。工人检查它的论点。如果[],则到达列表末尾并返回True。否则,它将剩余元素的头部与第一个元素进行比较,然后返回 False 或检查列表的其余部分。

其他三个功能。

  1. map 完全融合了 并且不分配临时 列表。
  2. 靠近定义的顶部 注意Cheap=True 声明。 这意味着 GHC 认为 功能“便宜”,因此 内联的候选人。打电话 站点,如果是具体的参数类型 可以确定,GHC大概会 内联allTheSame 并生成一个 非常紧的内环,完全 绕过Eq 字典 查找。
  3. 工作函数是 尾递归。

结论:非常强大的竞争者。

解决方案 1

allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)

即使不看核心,我也知道这不会那么好。该列表不止一次被遍历,首先是length xs,然后是length $ takeWhile。不仅你有多次遍历的额外开销,这意味着列表必须在第一次遍历后保留在内存中并且不能被 GC'd。对于一个大列表,这是一个严重的问题。

Test.allTheSame'
  :: forall a_abF. GHC.Classes.Eq a_abF => [a_abF] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 20 0}]
Test.allTheSame' =
  \ (@ a_awF)
    ($dEq_awG :: GHC.Classes.Eq a_awF)
    (xs_abI :: [a_awF]) ->
    case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
    case GHC.List.$wlen
           @ a_awF
           (GHC.List.takeWhile
              @ a_awF
              (let {
                 ds_sDq :: a_awF
                 [LclId, Str=DmdType]
                 ds_sDq =
                   case xs_abI of _ {
                     [] -> GHC.List.badHead @ a_awF; : x_axk ds1_axl -> x_axk
                   } } in
               \ (ds1_dxa :: a_awF) ->
                 GHC.Classes.== @ a_awF $dEq_awG ds1_dxa ds_sDq)
              xs_abI)
           0
    of ww1_XCn { __DEFAULT ->
    GHC.Prim.==# ww_aC6 ww1_XCn
    }
    }

看核心并不能说明太多。但是,请注意以下几行:

case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
        case GHC.List.$wlen

这是列表遍历发生的地方。第一个获取外部列表的长度并将其绑定到ww_aC6。第二个获取内部列表的长度,但直到底部附近才发生绑定,在

of ww1_XCn { __DEFAULT ->
GHC.Prim.==# ww_aC6 ww1_XCn

长度(Ints)可以拆箱并通过 primop 进行比较,但在引入开销之后,这是一个小小的安慰。

结论:不好。

解决方案 2

allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
    where  n = length xs

这和解决方案1有同样的问题。列表被遍历了多次,不能被GC'd。不过这里更糟,因为现在为每个子列表计算长度。我希望这在任何显着大小的列表中都具有最差的性能。另外,当您期望列表很大时,为什么要使用 1 和 2 元素的特殊情况列表?

结论:别想了。

解决方案 3

allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
  | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
    where n = length xs
          split = splitAt (n `div` 2) xs

这与解决方案2有相同的问题。即length多次遍历列表。我不确定分而治之的方法是解决这个问题的好选择,它最终可能比简单的扫描花费更长的时间。不过,这取决于数据,值得测试。

结论:也许,如果您使用不同的数据结构。

解决方案 4

allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)

这基本上是我的第一个想法。让我们再次检查内核。

Test.allTheSame''''
  :: forall a_abC. GHC.Classes.Eq a_abC => [a_abC] -> GHC.Bool.Bool
[GblId,
 Arity=2,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=IF_ARGS [3 3] 10 0}]
Test.allTheSame'''' =
  \ (@ a_am5)
    ($dEq_am6 :: GHC.Classes.Eq a_am5)
    (xs_alK :: [a_am5]) ->
    case xs_alK of _ {
      [] ->
        GHC.List.tail1
        `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
      : ds1_axJ xs1_axK ->
        GHC.List.all
          @ a_am5
          (\ (ds_dwU :: a_am5) ->
             GHC.Classes.== @ a_am5 $dEq_am6 ds_dwU ds1_axJ)
          xs1_axK
    }

好吧,还不错。与解决方案 1 一样,这将在空列表上出错。列表遍历隐藏在GHC.List.all 中,但它可能会在调用站点扩展为好的代码。

判决:另一个强有力的竞争者。

因此,在所有这些之间,我希望解决方案 0 和 4 是唯一值得使用的列表,它们几乎相同。在某些情况下,我可能会考虑选项 3。

编辑:在这两种情况下,空列表上的错误都可以像@augustss 的回答一样简单地修复。

下一步是使用criterion 进行一些时间分析。

【讨论】:

  • 哇!令人印象深刻的答案。我必须承认,我对核心代码一无所知:目前这对我来说太先进了。但是,您的解释非常有用,它们让我深入了解了我的代码中的许多方面和错误。谢谢!
  • 我不确定关于我认为不正确的向量的评论(尤其是对于大列表)并且有点傲慢,最重要的是不必要且与问题无关。
【解决方案4】:

这是另一个版本(不需要遍历整个列表以防万一不匹配):

allTheSame [] = True
allTheSame (x:xs) = isNothing $ find (x /= ) xs

这在语法上可能不正确,但我希望你明白了。

【讨论】:

  • 如果发现不匹配,解决方案 0 和 4 也不会遍历整个列表。
  • isNothing 可以用来缩短。
  • 很棒的另类想法,它还教会了我关于isNothingfind,我不知道。谢谢!
【解决方案5】:

使用连续对的解决方案:

allTheSame xs = and $ zipWith (==) xs (tail xs)

【讨论】:

  • 我建议使用drop 1 而不是tail,这样它也适用于空列表。
  • @dfeuer,实际上它适用于空列表,在这种情况下它不会被评估(因为zipWith 之前停止)。
  • 哦,是的,你是对的。对不起。 tail 我觉得还是有点混乱。
  • Pointfree: allTheSame = and . (zipWith (==) &lt;*&gt; tail)
  • 在性能方面与原始解决方案 0 相比如何?假设为空列表添加检查。
【解决方案6】:

我想我可能只是在实现find 并重做this。不过,我认为看看它的内部是很有启发性的。 (请注意解决方案如何依赖于等式是可传递的,但也要注意 问题 如何要求等式是可传递的以保持一致。)

sameElement x:y:xs = if x /= y then Nothing else sameElement y:xs
sameElement [x] = Just x
allEqual [] = True
allEqual xs = isJust $ sameElement xs

我喜欢sameElement 如何查看列表的前 O(1) 个元素,然后返回结果或递归列表的某些后缀,尤其是尾部。我对这种结构没什么好说的,我只是喜欢它:-)

我想我和this 做同样的比较。相反,如果我使用 sameElement x:xs 进行递归,我会将输入列表的头部与解决方案 0 中的每个元素进行比较。

切线:如果需要,可以通过将Nothing 替换为Left (x, y)Just x 替换为Right xisJust 替换为either (const False) (const True) 来报告两个不匹配的元素。

【讨论】:

    【解决方案7】:

    这是另一种有趣的方式:

    {-# INLINABLE allSame #-}
    allSame :: Eq a => [a] -> Bool
    allSame xs = foldr go (`seq` True) xs Nothing where
      go x r Nothing = r (Just x)
      go x r (Just prev) = x == prev && r (Just x)
    

    通过跟踪前一个元素,而不是第一个元素,此实现可以轻松更改为实现increasingdecreasing。要检查所有这些与第一个,您可以将prev 重命名为first,并将Just x 替换为Just first


    这将如何优化?我没有详细检查,但我将根据我对 GHC 优化的一些了解来讲述一个好故事。

    首先假设没有发生列表融合。然后foldr 将被内联,给出类似

    allSame xs = allSame' xs Nothing where
      allSame' [] = (`seq` True)
      allSame' (x : xs) = go x (allSame' xs)
    

    Eta 展开然后产生

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) acc = go x (allSame' xs) acc
    

    内联go

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) Nothing = allSame' xs (Just x)
    allSame' (x : xs) (Just prev) =
      x == prev && allSame' xs (Just x)
    

    现在 GHC 可以识别出递归调用中的 Maybe 值始终为 Just,并使用 worker-wrapper 转换来利用这一点:

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) Nothing = allSame'' xs x
    allSame' (x : xs) (Just prev) = x == prev && allSame'' xs x
    
    allSame'' [] prev = True
    allSame'' (x : xs) prev = x == prev && allSame'' xs x
    

    现在记住了

    allSame xs = allSame' xs Nothing
    

    并且allSame' 不再是递归的,因此可以进行 beta 缩减:

    allSame [] = True
    allSame (x : xs) = allSame'' xs x
    
    allSame'' [] _ = True
    allSame'' (x : xs) prev = x == prev && allSame'' xs x
    

    因此,高阶代码已变成高效的递归代码,无需额外分配。

    使用-O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures 编译定义allSame 的模块会产生以下结果(我已经清理了一下):

    allSame :: forall a. Eq a => [a] -> Bool
    allSame =
      \ (@ a) ($dEq_a :: Eq a) (xs0 :: [a]) ->
        let {
          equal :: a -> a -> Bool
          equal = == $dEq_a } in
        letrec {
          go :: [a] -> a -> Bool
          go =
            \ (xs :: [a]) (prev :: a) ->
              case xs of _ {
                [] -> True;
                : y ys ->
                  case equal y prev of _ {
                    False -> False;
                    True -> go ys y
                  }
              }; } in
        case xs0 of _ {
          [] -> True;
          : x xs -> go xs x
        }
    

    如您所见,这与我描述的结果基本相同。 equal = == $dEq_a 位是从 Eq 字典中提取相等方法并保存在变量中的位置,因此只需提取一次。


    如果列表融合发生会怎样?这里提醒一下定义:

    allSame xs = foldr go (`seq` True) xs Nothing where
      go x r Nothing = r (Just x)
      go x r (Just prev) = x == prev && r (Just x)
    

    如果我们调用allSame (build g),则foldr将根据foldr c n (build g) = g c n规则与build融合,产生

    allSame (build g) = g go (`seq` True) Nothing
    

    除非知道g,否则这不会让我们感兴趣。所以让我们选择一些简单的东西:

    replicate k0 a = build $ \c n ->
      let
        rep 0 = n
        rep k = a `c` rep (k - 1)
      in rep k0
    

    所以如果h = allSame (replicate k0 a)h变成了

    let
      rep 0 = (`seq` True)
      rep k = go a (rep (k - 1))
    in rep k0 Nothing
    

    Eta 扩展,

    let
      rep 0 acc = acc `seq` True
      rep k acc = go a (rep (k - 1)) acc
    in rep k0 Nothing
    

    内联go

    let
      rep 0 acc = acc `seq` True
      rep k Nothing = rep (k - 1) (Just a)
      rep k (Just prev) = a == prev && rep (k - 1) (Just a)
    in rep k0 Nothing
    

    再次,GHC 可以看到递归调用总是Just,所以

    let
      rep 0 acc = acc `seq` True
      rep k Nothing = rep' (k - 1) a
      rep k (Just prev) = a == prev && rep' (k - 1) a
      rep' 0 _ = True
      rep' k prev = a == prev && rep' (k - 1) a
    in rep k0 Nothing
    

    由于rep 不再是递归的,GHC 可以减少它:

    let
      rep' 0 _ = True
      rep' k prev = a == prev && rep' (k - 1) a
    in
      case k0 of
        0 -> True
        _ -> rep' (k - 1) a
    

    如您所见,它可以在没有任何分配的情况下运行!显然,这是一个愚蠢的例子,但类似的事情还会发生在更多有趣的案例中。例如,如果您编写一个AllSameTest 模块,导入allSame 函数并定义

    foo :: Int -> Bool
    foo n = allSame [0..n]
    

    并按照上面的描述编译它,你会得到以下(未清理)。

    $wfoo :: Int# -> Bool
    $wfoo =
      \ (ww_s1bY :: Int#) ->
        case tagToEnum# (># 0 ww_s1bY) of _ {
          False ->
            letrec {
              $sgo_s1db :: Int# -> Int# -> Bool
              $sgo_s1db =
                \ (sc_s1d9 :: Int#) (sc1_s1da :: Int#) ->
                  case tagToEnum# (==# sc_s1d9 sc1_s1da) of _ {
                    False -> False;
                    True ->
                      case tagToEnum# (==# sc_s1d9 ww_s1bY) of _ {
                        False -> $sgo_s1db (+# sc_s1d9 1) sc_s1d9;
                        True -> True
                      }
                  }; } in
            case ww_s1bY of _ {
              __DEFAULT -> $sgo_s1db 1 0;
              0 -> True
            };
          True -> True
        }
    
    foo :: Int -> Bool
    foo =
      \ (w_s1bV :: Int) ->
        case w_s1bV of _ { I# ww1_s1bY -> $wfoo ww1_s1bY }
    

    这可能看起来很恶心,但您会注意到在任何地方都没有 : 构造函数,并且 Ints 都未装箱,因此该函数可以在零分配的情况下运行。

    【讨论】:

      【解决方案8】:

      虽然效率不高(即使前两个元素不匹配,它也会遍历整个列表),这是一个厚颜无耻的解决方案:

      import Data.List (group)
      
      allTheSame :: (Eq a) => [a] -> Bool
      allTheSame = (== 1) . length . group
      

      只是为了好玩。

      【讨论】:

      • 这可能很有趣,但真的很糟糕。首先,它不起作用:group 需要对元素进行排序才能工作。其次,在组结果上使用长度意味着您必须计算所有组。列表中更好的模式匹配。
      • 确实工作:因为你只是在寻找有一个组的情况,你不需要任何排序!是的,它效率低下。我已经说过了。
      • 我的错误。尽管如此,它还是效率低下,-)
      • null . tail . group,除非它在空列表上不起作用
      【解决方案9】:

      这个实现是优越的。

      allSame [ ] = True
      allSame (h:t) = aux h t
      
      aux x1 [ ]                 = True
      aux x1 (x2:xs) | x1==x2    = aux x2 xs 
                     | otherwise = False
      

      鉴于 (==) 运算符的传递性,假设 Eq 的实例得到很好的实现,如果您希望确保表达式链的相等性,例如 a = b = c = d,您只需确保a=b, b=c, c=d, and that d=a, 代替上面提供的技术,例如 a=b, a=c, a=d, b=c, b=d, c=d .

      我提出的解决方案会随着您希望测试的元素数量线性增长,后者是二次方的,即使您引入常数因子以希望提高其效率。

      它也优于使用组的解决方案,因为您最终不必使用长度。

      你也可以用逐点的方式写得很好,但我不会用这些琐碎的细节让你厌烦。

      【讨论】:

      • 你能补充解释吗?为什么这个实现更好?
      • @O.O.Balance 我刚刚做了。希望对您有所帮助。
      • 这和方案#0和方案#4基本一样,对吧,只是展开了?
      猜你喜欢
      • 2011-04-20
      • 1970-01-01
      • 1970-01-01
      • 2022-01-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-19
      • 1970-01-01
      相关资源
      最近更新 更多