Haskell group依赖于累加器值

时间:2017-10-31 23:12:31

标签: algorithm haskell functional-programming fold

我有一组视图对,它们代表内容标签列表及其宽度我希望按行分组(如果下一个内容标签不符合行,则将其放入另一行)。所以我们有:viewList = [(View1, 45), (View2, 223.5), (View3, 14) (View4, 42)]

我想编写一个函数groupViews :: [a] -> [[a]]来将此列表分组到一个子列表列表中,其中每个子列表仅包含宽度小于最大指定宽度的视图(假设为250)。 因此,对于已排序的viewList,此函数将返回:[[(View3, 14), (View4, 42), (View1, 45)],[(View2, 223.5)]]

看起来类似于groupBy。但是,groupBy不维护累加器。我尝试使用scanl + takeWhile(<250)组合,但在这种情况下,我只能收到第一个有效的子列表。也许以某种方式使用iterate + scanl + takeWhile?但这看起来非常麻烦,根本不起作用。任何帮助将不胜感激。

3 个答案:

答案 0 :(得分:4)

我会从这样的递归定义开始:

groupViews :: Double -> (a -> Double) -> [a] -> [[a]]
groupViews maxWidth width = go (0, [[]])
  where
    go (current, acc : accs) (view : views)
      | current + width view <= maxWidth
      = go (current + width view, (view : acc) : accs) views
      | otherwise = go (width view, [view] : acc : accs) views
    go (_, accs) []
      = reverse $ map reverse accs

像[{1}}一样调用。我注意到的第一件事是它可以表示为左折:

groupViews 250 snd (sortOn snd viewList)

我认为这很好,但如果你愿意的话可以进一步考虑,在一次扫描中累积宽度以模数最大宽度,另一次传递将元素分组为升序。例如,这是一个适用于整数宽度的版本:

groupViews' maxWidth width
  = reverse . map reverse . snd . foldl' go (0, [[]])
  where
    go (current, acc : accs) view
      | current + width view <= maxWidth
      = (current + width view, (view : acc) : accs)
      | otherwise
      = (width view, [view] : acc : accs)

当然,您可以在这些定义中包含排序,而不是从外部传递排序列表。

答案 1 :(得分:3)

我不知道通过组合标准库中的函数来实现这一目的的聪明方法,但我认为你可以做得比从头开始实现它更好。

这个问题适合我以前见过的一类问题:“以某种方式从这个列表中批量处理项目,并根据某些组合规则将其项目合并为批次,并根据某个规则来确定批次何时过大” 。多年前,当我写Clojure时,我built a function抽象出了这种批量组合的想法,只是要求你指定批处理的规则,并且能够在令人惊讶的数量的地方使用它。

以下是我认为可能在Haskell中重新构想的方式:

glue :: Monoid a => (a -> Bool) -> [a] -> [a]
glue tooBig = go mempty
  where go current [] = [current]
        go current (x:xs) | tooBig x' = current : go x xs
                          | otherwise = go x' xs
          where x' = current `mappend` x

如果你已经有这样的glue函数,你可以用适当的Monoid实例(对象列表及其累积和)构建一个简单的数据类型,然后让{{1}做重物:

glue

一方面,对于一个简单的函数来说,这看起来相当多,但另一方面,有一个描述你正在进行的累积求和的类型是相当不错的,而且Monoid实例无论如何都很好。 ..在定义类型和Monoid实例之后,在调用import Data.Monoid (Sum(..)) data ViewGroup contents size = ViewGroup {totalSize :: size, elements :: [(contents, size)]} instance Monoid b => Monoid (ViewGroup a b) where mempty = ViewGroup mempty [] mappend (ViewGroup lSize lElts) (ViewGroup rSize rElts) = ViewGroup (lSize `mappend` rSize) (lElts ++ rElts) viewGroups = let views = [("a", 14), ("b", 42), ("c", 45), ("d", 223.5)] in glue ((> 250) . totalSize) [ViewGroup (Sum width) [(x, Sum width)] | (x, width) <- views] main = print (viewGroups :: [ViewGroup String (Sum Double)]) [ViewGroup {totalSize = Sum {getSum = 101.0}, elements = [("a",Sum {getSum = 14.0}), ("b",Sum {getSum = 42.0}), ("c",Sum {getSum = 45.0})]}, ViewGroup {totalSize = Sum {getSum = 223.5}, elements = [("d",Sum {getSum = 223.5})]}] 本身时几乎没有工作要做。

好吧,我不知道,也许这仍然是太多的工作,特别是如果你不相信你可以重用那种类型。但我认为认识到这是一个更普遍的问题的特定情况是有用的,并尝试解决更普遍的问题。

答案 2 :(得分:2)

鉴于groupViews'' maxWidth width views = map fst $ groupBy ((<) `on` snd) $ zip views $ drop 1 $ scanl (\ current view -> (current + width view) `mod` maxWidth) 0 views groupBy本身是由手动递归函数定义的,我们修改的函数将使用相同的机制。

让我们首先定义一个通用函数span,它取累加器的初始值,然后是一个函数,它接受列表中的元素,当前累加器状态并可能产生新的累积值(Nothing表示不接受该元素):

groupAcc

对于我们的具体问题,我们定义:

{-# LANGUAGE LambdaCase #-}

import Data.List (sortOn)
import Control.Arrow (first, second)

spanAcc :: z -> (a -> z -> Maybe z) -> [a] -> ((z, [a]), [a])
spanAcc z0 p = \case
  xs@[]      -> ((z0, xs), xs)
  xs@(x:xs') -> case p x z0 of
    Nothing  -> ((z0, []), xs)
    Just z1  -> first (\(z2, xt) -> (if null xt then z1 else z2, x : xt)) $
                spanAcc z1 p xs'

groupAcc :: z -> (a -> z -> Maybe z) -> [a] -> [(z, [a])]
groupAcc z p = \case
  [] -> [] ;
  xs -> uncurry (:) $ second (groupAcc z p) $ spanAcc z p xs

最终给了我们:

threshold :: (Num a, Ord a) => a -> a -> a -> Maybe a
threshold max a z0 = let z1 = a + z0 in if z1 < max then Just z1 else Nothing

groupViews :: (Ord z, Num z) => [(lab, z)] -> [[(lab, z)]]
groupViews = fmap snd . groupAcc 0 (threshold 250 . snd)

ghci给了我们:

groupFinal :: (Num a, Ord a) => [(lab, a)] -> [[(lab, a)]]
groupFinal = groupViews . sortOn snd

如果我们愿意,我们可以通过假设> groupFinal [("a", 45), ("b", 223.5), ("c", 14), ("d", 42)] [[("c",14.0),("d",42.0),("a",45.0)],[("b",223.5)]] groupAccz来简化Monoid,因此可以使用mempty,这样:

groupAcc2 :: Monoid z => (a -> z -> Maybe z) -> [a] -> [(z, [a])]
groupAcc2 p = \case
  [] -> [] ;
  xs -> let z = mempty in
        uncurry (:) $ second (groupAcc z p) $ spanAcc z p xs