基本上,我会将此描述为[(,)]
与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)]
答案 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)
尽管如此,最好将merge
和accum
作为公开的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包中找到,称为mergeBy和mergeAllBy。
编辑:最近新的优先级队列实施published on Hackage。请参阅this reddit thread中有关它的讨论。