将两个关联列表与正在运行的累加器合并

时间:2014-06-23 04:28:57

标签: haskell

基本上,我会将此描述为[(,)]snd上的正在运行的累加器相结合的联合/合并...是否有一种优雅的方式来实现它?

(请仅在回答问题的上下文中引用我的代码。如果您想查看我的代码,那也会很棒,但请在其他网站上执行此操作:https://codereview.stackexchange.com/questions/54993/merging-time-series

时间序列,

data Model a where
  Variant  :: [(Day, a)] -> Model a
  deriving (Show)

... a中的[(Day, a)]类型基本上代表“总余额”,例如银行账户。

一些示例数据,

day1 = fromGregorian 1987 10 17
day2 = fromGregorian 1987 10 18
day3 = fromGregorian 1987 10 19
day4 = fromGregorian 1987 10 20
day5 = fromGregorian 1987 10 21
day6 = fromGregorian 1987 10 22

m1 = Variant [(day1, 1), (day3, 3), (day5, 5)] :: Model Integer
m2 = Variant [(day1, 1), (day2, 2), (day4, 4), (day6, 6)] :: Model Integer

现在,合并两个时间序列,使“总余额”为加法,

(&+) :: Num a => Model a -> Model a -> Model a
(Variant a) &+ (Variant b) = Variant $ reverse $ fst $ go a b ([],0)
  where
    go             []             [] (xs, c) = (xs, c)
    go   ((da,va):as)             [] (xs, c) = go as [] (((da,va+c):xs), va+c)
    go             []   ((db,vb):bs) (xs, c) = go [] bs (((db,vb+c):xs), vb+c)
    go a@((da,va):as) b@((db,vb):bs) (xs, c)
      | da > db  = go  a bs (((db,vb+c):xs), vb+c)
      | da < db  = go as  b (((da,va+c):xs), va+c)
      | da == db = go as bs (((da,va+vb+c):xs), va+vb+c)

所以,

what = m1 &+ m2

Variant [(1987-10-17,2),(1987-10-18,4),(1987-10-19,7),(1987-10-20,11),(1987-10-21,16),(1987-10-22,22)]

2 个答案:

答案 0 :(得分:5)

我看到reverse的那一刻,我觉得可能有麻烦。这是一个更懒惰的版本,适用于无限值。但是,它依赖于每个输入按Day排序。首先,我们寻求merge两个流

merge :: Num a => Model a -> Model a -> Model a
merge (Variant xs) (Variant ys) = Variant (go xs ys) where
  go [] ys = ys
  go xs [] = xs
  go xx@((dx, vx):xs) yy@((dy, vy):ys)
    | dx < dy   = (dx, vx)      : go xs yy
    | dx > dy   = (dy, vy)      : go xx ys
    | otherwise = (dx, vx + vy) : go xs ys

它基本上是你拥有的核心,但更简单。通常,如果你可以在Haskell中进行懒惰计算,那么它值得付出努力,因为它可能更有效。在此之后,我们将积累

accum :: Num a => Model a -> Model a
accum (Variant xs) = Variant (go xs 0) where
  go []          _ = []
  go ((d, v):xs) c = let c' = v + c in (d, c') : go xs c'

然后结合这两个,我们得到了理想的结果

-- (&+) :: Num a => Model a -> Model a -> Model a
-- a &+ b = accum (merge a b)

尽管如此,最好将mergeaccum作为公开的API,因为除了(&+)之外,它们可以通过多种方式进行组合。


值得注意的是,将accum函数写为右折叠的显而易见的方法

accum' :: Num a => Model a -> Model a
accum' (Variant xs) = Variant (snd $ foldr go (0, []) xs) where
  go (d, v) (c, res) = let c' = v + c in (c', (d, c'):res)

不起作用,因为它累积了列表后面的参数。尝试左侧折叠可以工作,但我们必须颠倒这个列表 - 一个双重罪恶的懒惰。

accum'' :: Num a => Model a -> Model a
accum'' (Variant xs) = Variant (reverse $ snd $ foldl go (0, []) xs) where
  go (d, v) (c, res) = let c' = v + c in (c', (d, c'):res)

提供了原始版本中发生的事情的提示。但是,我们可以把它写成一个正确的折叠,但是为了在正确的方向上传递累加器,我们必须有点棘手

accum' :: Num a => Model a -> Model a
accum' (Variant xs) = Variant (foldr go (const []) xs 0) where
  go (d, v) rest c = let c' = v + c in (d, c') : rest c'

请注意,foldr go (const []) xs的结果是a -> [a]类型的值。

答案 1 :(得分:2)

这里的关联列表实际上是一个&#34;红鲱鱼&#34;。这实际上是一个更普遍的问题,即如何使用函数将项与相等键组合进行合并。关联列表版本是相同的问题,但预先应用了Schwartzian transform

这样说,我们想要一个这种类型的函数:

mergeCombineWith :: (a -> a -> Ordering) -> (a -> a -> a) -> [a] -> [a] -> [a]

其中第一个参数定义排序,第二个参数是要应用于具有相等键的元素的组合函数。我们假设输入列表是预先排序的。如果我们还假设输入列表都没有任何重复键,或者我们也想在同一输入列表中组合重复项,那么解决方案很简单。给定传统的合并函数,类型为:

mergeWith :: (a -> a -> Ordering) -> [a] -> [a] -> [a]

然后通过对传统合并的结果进行分组来获得我们期望的函数:

mergeCombineWith cmp comb xs ys =
    map combs . groupBy eq $ mergeWith cmp xs ys
  where
    combs = foldr1 comb
    eq x y = isEQ $ cmp x y
    isEQ EQ = True
    isEQ _  = False

更一般地说,考虑合并许多列表而不仅仅是两个列表会很有趣。这可以使用折叠以简单的方式完成:

multiMergeCombineWith :: (a -> a -> Ordering) -> (a -> a -> a) -> [[a]] -> [a]
multiMergeCombineWith cmp comb = foldr1 $ mergeCombineWith cmp comb

但是,如果要合并许多列表,那么该解决方案效率会很低。更好的方法是将列表放入priority queue并始终首先检查其给定排序中第一个元素最小的列表。 Hackage上有几个优秀的优先级队列实现。

然而,再一次,如果你有一个传统合并的多列表问题的解决方案,你不需要重新发明轮子。首先进行传统的合并,然后进行分组和组合,如上所述。

感谢Daniel Wagner向我指出两个传统合并函数的版本可以在Hackage的data-ordlist包中找到,称为mergeBymergeAllBy

编辑:最近新的优先级队列实施published on Hackage。请参阅this reddit thread中有关它的讨论。