【问题标题】:Why the "++" function is much more expensive than ":"? [duplicate]为什么“++”函数比“:”贵很多? [复制]
【发布时间】:2019-02-18 06:13:59
【问题描述】:

以下段落来自Learn You a Haskell for Great Good!

“在长字符串上重复使用 ++ 运算符时要小心。当您将两个列表放在一起时(即使您将单例列表附加到列表中,例如:[1,2,3] ++ [4] ),在内部,Haskell 必须遍历 ++ 左侧的整个列表。在处理不太大的列表时,这不是问题。但是在一个有 5000 万个条目的列表末尾放置一些东西是需要一些时间。但是,使用 : 运算符(也称为 cons 运算符)将某些内容放在列表的开头是即时的。"

我不知道为什么 Haskell 必须遍历 ++ 左侧的整个列表。

【问题讨论】:

  • 你知道列表是在 Haskell 中实现的吗? (++) 还能如何工作?
  • 否则你将如何构建一个列表?
  • @Carl 我知道 [1,2,3] 实际上只是 1:2:3:[] 的语法糖
  • @GovindParmar 这个问题明确与性能有关(而另一个不是),这里的答案反映了通过以更有效的方式解决该方面的问题。我会把它打开,除非有更好的匹配和很好的答案。

标签: haskell


【解决方案1】:

右轴(右侧)的列表必须在左轴的最后一个元素之后。由于 haskell 列表是根据继任者实现的,因此您需要“到达”最后一个元素以将任何内容附加到它,即使您要附加的列表 its 继任者。

如果您只存储对第一个元素的引用,这类似于在命令式语言中连接单链表。您只能附加到最后一个,但要找到它,您需要遍历所有链接。

如果你实现自己的列表,这会因为语法变化而变得更加明显:

data List a = Empty | Cons a (List a)

Cons 1 (Cons 2 (Cons 3 Empty)))

要附加到这样的列表,您需要更改*中间的Empty。但是查看“从外部”的值(例如通过模式匹配),您只会看到Cons 1 <tail>tail 部分是一些模糊的东西,直到你评估它并看到 Cons 2 <tail> 等等,这是你试图避免的。

相反,前置只是Cons 0 <the list above>,将整个列表包装起来,甚至不看它。这就是为什么你可以写0 : [1..]这样的东西,但不能写[1..] ++ [42]这样的东西。


* 创建一个在该特定点上有所不同的新列表。 Haskell 列表(一般的值)显然是不可变的。

【讨论】:

    【解决方案2】:

    我认为除了其他答案之外,手动展开可能是一个有用的工具。列表[1,2,3] 表示为“conses” (:) 的右嵌套表达式,如下所示

    1 : (2 : (3 : []))
    

    我们知道 : 运算符关联到右侧,所以这里只是写了

    1:2:3:[]
    

    Append 由这两种模式定义:

    []     ++ ys = ys
    (x:xs) ++ ys = x : (xs ++ ys)
    

    所以现在让我们看看[1,2,3] ++ [4,5] 是如何展开的。从重写为 conses 开始:

    (1:2:3:[]) ++ (4:5:[])
    --                    (second pattern, x = 1, xs = 2:3:[], ys = 4:5:[])
    1 : ((2:3:[]) ++ (4:5:[]))
    --                    (second pattern, x = 2, xs = 3:[], ys = 4:5:[])
    1 : 2 : ((3:[]) ++ (4:5:[]))
    --                    (second pattern, x = 3, xs = [], ys = 4:5:[])
    1 : 2 : 3 : ([] ++ (4:5:[]))
    --                    (first pattern, ys = 4:5:[])
    1 : 2 : 3 : 4 : 5 : []
    

    这就是它的工作原理。看看我们是如何遍历左侧列表的,但我们不需要遍历右侧列表?从某种意义上说,我们遍历左侧列表是为了找到它末尾的[],然后将末尾的[]替换为对整个右侧列表的引用。

    【讨论】:

      【解决方案3】:

      在 Haskell 中类似 [3,1,5,6] 的列表如下所示:

      3 : (1 : (5 : [6])),其中: 是 cons 函数。显然你仍然可以在 Haskell 中编写像 [3,1,5,6] 这样的列表,但这只是花哨的语法。

      如您所见,在 6 之后添加一个元素很困难:6 一直嵌套在列表的下方。为了添加项目,Haskell 需要完全解构列表才能添加项目,如下所示:

      (++) (x : xs) otherList = x : (xs ++ otherList)
      (++) [x] otherList = x : otherList
      

      这很可能不是(++) 的实际定义,但它说明了问题。 (++) 操作符必须遍历整个最左边的列表,以便将列表中的最后一项连接到新列表,然后递归地再次连接所有其他项。

      【讨论】:

      • : 比返回列表的函数更接近列表文字。 ++ 的返回值没有留下任何原始参数的痕迹,而: created 的值存储了它的参数。 1 : () 列表,而不是计算结果为列表的表达式。
      • 是的,这也是我的理解,您为什么觉得有必要对此发表评论?还是你在扩展我所说的?我在我的答案中找不到与此相矛盾的东西。 :List 的构造函数,使其成为函数。
      猜你喜欢
      • 1970-01-01
      • 2014-05-18
      • 1970-01-01
      • 2011-06-18
      • 2021-01-08
      • 2010-12-12
      • 2011-01-05
      • 2014-04-11
      • 1970-01-01
      相关资源
      最近更新 更多