(++)与懒惰评估的表现

时间:2012-09-06 09:11:23

标签: haskell concatenation lazy-evaluation

我一直在想这个问题,但我找不到令人满意的答案。

为什么(++)“贵”?在延迟评估下,我们不会评估像

这样的表达式
xs ++ ys

在必要之前,即使这样,我们也只会评估我们需要的部分 时我们需要它们。

有人可以解释我错过的东西吗?

3 个答案:

答案 0 :(得分:15)

如果访问整个结果列表,延迟评估将不会保存任何计算。它只会延迟它,直到你需要每个特定的元素,但最后,你必须计算相同的东西。

如果在遍历级联列表xs ++ ys,访问所述第一部分中的每个元素(xs)增加了一个小恒定的开销,检查是否xs的花费或不

因此,如果您将++与左侧或右侧相关联,则会产生很大的不同。

  • 如果您将长度为n的{​​{1}}列表与k之类的左侧相关联,则会访问每个首先(..(xs1 ++ xs2) ... ) ++ xsn个元素将花费k时间,访问下一个O(n)每个k将会花费O(n-1)等。因此遍历整个列表将需要O(k n^2)。你可以检查一下

    sum $ foldl (++) [] (replicate 100000 [1])
    

    真的长。

  • 如果您将n长度k列表与正确相关联,例如xs1 ++ ( ..(xsn_1 ++ xsn) .. ),则每个元素只会获得不变的开销,所以遍历整个列表只会O(k n)。你可以检查一下

    sum $ foldr (++) [] (replicate 100000 [1])
    

    非常合理。


编辑:这只是隐藏在ShowS背后的魔力。如果您将每个字符串xs转换为showString xs :: String -> StringshowString只是(++)的别名)并撰写这些函数,那么无论您如何关联它们的组成,最后它们将从右到左应用 - 正是我们需要获得线性时间复杂度。 (这只是因为(f . g) xf (g x)。)

您可以检查两者

length $ (foldl (.) id (replicate 1000000 (showString "x"))) ""

length $ (foldr (.) id (replicate 1000000 (showString "x"))) ""

在合理的时间内运行(foldr有点快,因为从右侧编写函数时它的开销较小,但两者在元素数量上都是线性的。)

答案 1 :(得分:4)

它本身并不太昂贵,当你开始从左到右组合大量的++时会出现问题:这样的链被评估为

  ( ([1,2] ++ [3,4]) ++ [5,6] ) ++ [7,8]
≡ let a = ([1,2] ++ [3,4]) ++ [5,6]
        ≡ let b = [1,2] ++ [3,4]
                ≡ let c = [1,2]
                  in  head c : tail c ++ [3,4]
                    ≡ 1 : [2] ++ [3,4]
                    ≡ 1 : 2 : [] ++ [3,4]
                    ≡ 1 : 2 : [3,4]
                    ≡ [1,2,3,4]
          in  head b : tail b ++ [5,6]
            ≡ 1 : [2,3,4] ++ [5,6]
            ≡ 1:2 : [3,4] ++ [5,6]
            ≡ 1:2:3 : [4] ++ [5,6]
            ≡ 1:2:3:4 : [] ++ [5,6]
            ≡ 1:2:3:4:[5,6]
            ≡ [1,2,3,4,5,6]
  in head a : tail a ++ [7,8]
   ≡ 1 : [2,3,4,5,6] ++ [7,8]
   ≡ 1:2 : [3,4,5,6] ++ [7,8]
   ≡ 1:2:3 : [4,5,6] ++ [7,8]
   ≡ 1:2:3:4 : [5,6] ++ [7,8]
   ≡ 1:2:3:4:5 : [6] ++ [7,8]
   ≡ 1:2:3:4:5:6 : [] ++ [7,8]
   ≡ 1:2:3:4:5:6 : [7,8]
   ≡ [1,2,3,4,5,6,7,8]

您可以清楚地看到二次复杂度。即使你只想评估 n -th元素,你仍然需要通过所有那些let来挖掘你的方法。这就是++infixr的原因,因为[1,2] ++ ( [3,4] ++ ([5,6] ++ [7,8]) )实际上效率更高。但是,如果你在设计时不小心,比如一个简单的序列化器,你可能很容易就会得到一个像上面那样的链。这是初学者被警告++的主要原因。

除此之外,Prelude.++与例如Bytestring相比较慢。 ++操作的原因很简单,它的工作原理是遍历链表,这些链表总是次优的缓存使用等,但这并不是问题;这可以防止你实现类似C的性能,但只使用普通列表正确编写的程序,{{1}}仍然可以很容易地与蟒。

答案 2 :(得分:2)

我想在Petr的回答中添加一两件事。

正如他所指出的那样,一开始就反复追加清单是相当便宜的,而追加到底部却不是。只要您使用haskell的列表,就是这样。 但是,在某些情况下,您必须追加到最后(例如,您正在构建要打印的字符串)。使用常规列表,您必须处理他的答案中提到的二次复杂性,但在这些情况下有更好的解决方案:difference lists(另请参阅关于该主题的my question)。

长话短说,通过将列表描述为函数的组合而不是较短列表的串联,您可以在常量时间内通过组合函数在差异列表的开头或末尾追加列表或单个元素。完成后,您可以在线性时间(元素数量)中提取常规列表。