惯用的高效Haskell追加?

惯用的高效Haskell追加?,haskell,performance,linked-list,append,idioms,Haskell,Performance,Linked List,Append,Idioms,List和cons操作符(:)在Haskell中非常常见。康斯是我们的朋友。但有时我想把它添加到列表的末尾 xs `append` x = xs ++ [x] 遗憾的是,这并不是一种有效的实施方式 我用Haskell写作,但我不得不使用+[x]反惯用语: ptri = [1] : mkptri ptri mkptri (row:rows) = newRow : mkptri rows where newRow = zipWith (+) row (0:row) ++ [1] 嗯,这是

List和cons操作符
(:)
在Haskell中非常常见。康斯是我们的朋友。但有时我想把它添加到列表的末尾

xs `append` x = xs ++ [x]
遗憾的是,这并不是一种有效的实施方式

我用Haskell写作,但我不得不使用
+[x]
反惯用语:

ptri = [1] : mkptri ptri
mkptri (row:rows) = newRow : mkptri rows
    where newRow = zipWith (+) row (0:row) ++ [1]
嗯,这是一个可爱的可读的帕斯卡三角形和所有,但反习语烦我。有人能给我解释一下(理想情况下,给我一个好的教程)对于您希望高效地附加到末尾的情况,惯用的数据结构是什么吗?我希望在这个数据结构及其方法中有类似列表的美。或者,也可以向我解释一下,为什么这个反习惯用法在这个案例中并没有那么糟糕(如果你认为是这样的话)


[编辑]我最喜欢的答案是
Data.Sequence
,它确实具有“近似列表的美”。我不确定我对所需操作的严格性有何感受。欢迎提出进一步建议和不同意见

import Data.Sequence ((|>), (<|), zipWith, singleton)
import Prelude hiding (zipWith)

ptri = singleton 1 : mkptri ptri

mkptri (seq:seqs) = newRow : mkptri seqs
    where newRow = zipWith (+) seq (0 <| seq) |> 1

导入数据。序列((|>),(标准
序列
有O(1)用于从“两端”添加,O(log(min(n1,n2)))用于一般连接:


与列表不同的是,
序列
是严格的

Chris Okasaki设计了一个队列来解决这个问题。参见他的论文第15页

您可能需要稍微调整代码,但使用反向操作并保留列表中的两个部分可以使您的工作效率平均提高

还有,有人在monad reader中提供了一些有效操作的列表代码。我承认,我并没有真正理解它,但我认为如果我集中精力,我可以找到它。原来是Douglas M.Auclair在monad reader第17期中写的


我意识到上面的答案并不能直接解决这个问题。所以,对于傻笑来说,这是我的递归答案。请随意把它撕开——它不漂亮

import Data.List 

ptri = [1] : mkptri ptri

mkptri :: [[Int]] -> [[Int]]
mkptri (xs:ys) =  mkptri' xs : mkptri ys

mkptri' :: [Int] -> [Int]
mkptri' xs = 1 : mkptri'' xs

mkptri'' :: [Int] -> [Int]
mkptri'' [x]        = [x]
mkptri'' (x:y:rest) = (x + y):mkptri'' (y:rest)

根据您的用例,
ShowS
方法(通过函数组合进行追加)可能会有用。

类似这种显式递归的方法可以避免追加“反惯用法”。尽管如此,我认为它没有您的示例那么清楚

ptri = []:mkptri ptri
mkptri (xs:ys) = pZip xs (0:xs) : mkptri ys
    where pZip (x:xs) (y:ys) = x+y : pZip xs ys
          pZip [] _ = [1]

我写了一个@geekosaur的
ShowS
方法的例子。你可以在中看到许多
ShowS
的例子


[编辑]根据@Dan的想法,我用zipWithS重写了newRow

newRow :: [Int] -> [Int] -> [Int]
newRow xs = zipWithS (+) xs (0:xs) . (1:)

zipWithS :: (a -> b -> c) -> [a] -> [b] -> [c] -> [c]
zipWithS z (a:as) (b:bs) xs =  z a b : zipWithS z as bs xs
zipWithS _ _ _ xs = xs

如果您只需要廉价的append(concat)和snoc(右边的cons)Hughes列表,也称为DList on Hackage,是最简单的实现方法。如果你想知道它们是如何工作的,看看Andy Gill和Graham Hutton的第一张工人包装纸,John Hughes的原始文件似乎不在线。正如其他人所说,上面显示的是一个字符串专用Hughes列表/DList

JoinList需要更多的工作来实现。这是一个二叉树,但有一个列表API-concat和snoc很便宜,你可以合理地对其进行fmap:Hackage上的DList有一个functor实例,但我认为它不应该有-functor实例必须在常规列表中进行变换。如果你想要一个JoinList,那么你需要使用wn-关于Hackage的是我的,它既没有效率,也写得不好


Data.Sequence具有高效的cons和snoc,并且适用于JoinList比较慢的其他操作-Take、Drop等。因为Data.Sequence的内部finger树实现必须平衡树,append比其JoinList等价物要做更多的工作。实际上,由于Data.Sequence编写得更好,我希望它的性能仍然优于JoinListorms我的附加JoinList。

如果您正在寻找通用解决方案,那么以下内容如何:

mapOnto :: [b] -> (a -> b) -> [a] -> [b]
mapOnto bs f = foldr ((:).f) bs
这为地图提供了一个简单的替代定义:

map = mapOnto []
对于其他基于foldr的函数,我们可以使用类似的定义,例如zipWith:

zipOntoWith :: [c] -> (a -> b -> c) -> [a] -> [b] -> [c]
zipOntoWith cs f = foldr step (const cs)
  where step x g [] = cs
        step x g (y:ys) = f x y : g ys
再次轻松推出zipWith和zip:

zipWith = zipOntoWith []
zip = zipWith (\a b -> (a,b))
现在,如果我们使用这些通用函数,您的实现 变得非常简单:

ptri :: (Num a) => [[a]]
ptri = [] : map mkptri ptri
  where mkptri xs = zipOntoWith [1] (+) xs (0:xs)

另一种方法是通过使用无限列表来避免连接:

ptri = zipWith take [0,1..] ptri'
  where ptri' = iterate stepRow $ repeat 0
        stepRow row = 1 : zipWith (+) row (tail row)

我不一定把你的代码称为“反idomatic”。通常,越清晰越好,即使这意味着牺牲几个时钟周期

在您的特定情况下,末尾的追加实际上并没有改变big-O时间复杂性!计算表达式

zipWith (+) xs (0:xs) ++ [1]

将需要时间成比例的
长度xs
,并且没有花哨的序列数据结构会改变这一点。如果有的话,只有常量因子会受到影响。

请记住,看起来很差的渐近性实际上可能不是,因为您使用的是一种惰性语言。在严格的语言中,在链接li的末尾追加在这种情况下,st永远是O(n)。在懒惰的语言中,只有当你真正遍历到列表的末尾时,它才是O(n),在这种情况下,你无论如何都会花费O(n)的努力。所以在很多情况下,懒惰可以拯救你


这并不是一个保证……例如,k个附加项后跟一个遍历仍然会在O(nk)中运行,而它可能在O(n+k)中运行。但它确实在某种程度上改变了情况。当结果立即强制执行时,从单个操作的渐进复杂性角度考虑它们的性能并不总是最终给出正确的答案。

在帕斯卡三角形的代码中,++[x]这实际上不是问题。因为您必须在++的左侧生成一个新列表,所以您的算法本质上是二次的;您不能仅仅通过避免++而使其渐进地更快


另外,在这种特殊情况下,当编译-O2时,GHC的列表融合规则(应该)删除++通常创建的列表副本。这是因为zipWith在其第一个参数中是一个好的生产者,++是一个好的消费者。您可以在中阅读这些优化。

您可以将列表表示为一个函数,以便从[]构建列表

list1, list2 :: [Integer] -> [Integer]
list1 = \xs -> 1 : 2 : 3 : xs
list2 = \xs -> 4 : 5 : 6 : xs
然后,您可以轻松地附加列表并添加到任意一端

list1 . list2 $ [] -> [1,2,3,4,5,6]
list2 . list1 $ [] -> [4,5,6,1,2,3]
(7:) . list1 . (8:) . list2 $ [9] -> [7,1,2,3,8,4,5,6,9]
你可以重写zipWi
list1 . list2 $ [] -> [1,2,3,4,5,6]
list2 . list1 $ [] -> [4,5,6,1,2,3]
(7:) . list1 . (8:) . list2 $ [9] -> [7,1,2,3,8,4,5,6,9]
zipWith' _ [] _ = id
zipWith' _ _ [] = id
zipWith' f (x:xs) (y:ys) = (f x y :) . zipWith' f xs ys
ptri = [] : mkptri ptri
mkptri (xs:yss) = newRow : mkptri yss
    where newRow = zipWith' (+) xs (0:xs) [1]
ptri = ([] : ) . map ($ []) . iterate (\x -> zipWith' (+) (x [0]) (0 : x [])) $ (1:)
ptri = [] : iterate (\x -> 1 : zipWith' (+) (tail x) x [1]) [1]
ptri = [] : iterate (uncurry (:) . mapAccumR (\x x' -> (x', x+x')) 0) [1]