【问题标题】:groupBy-like function such that the binary predicate holds between consecutive elements of each group instead of any twogroupBy-like 函数,使得二元谓词保持在每个组的连续元素之间,而不是任何两个
【发布时间】:2020-10-07 16:36:45
【问题描述】:

在 Hackage 上,我看到 groupBy's implementation 是这样的:

groupBy                 :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy _  []           =  []
groupBy eq (x:xs)       =  (x:ys) : groupBy eq zs
                           where (ys,zs) = span (eq x) xs

这意味着谓词eq 位于每组的任意两个元素之间。例子:

> difference_eq_1 = ((==1).) . flip (-)
> first_isnt_newline = ((/= '\n').) . const
>
> Data.List.groupBy difference_eq_1 ([1..10] ++ [11,13..21])
[[1,2],[3,4],[5,6],[7,8],[9,10],[11],[13],[15],[17],[19],[21]]
>
> Data.List.groupBy first_isnt_newline "uno\ndue\ntre"
["uno\ndue\ntre"]

如果我想对元素进行分组,以使谓词在任意一对连续元素之间成立,那么上述结果会如下所示?

[[1,2,3,4,5,6,7,8,9,10,11],[13],[15],[17],[19],[21]]
["uno\n","due\n","tre"]

我自己写的,看起来有点丑

groupBy' :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy' p = foldr step []
  where step elem [] = [[elem]]
        step elem gs'@((g'@(prev:g)):gs)
          | elem `p` prev = (elem:g'):gs
          | otherwise = [elem]:gs'

所以我在徘徊,如果这样的功能已经存在,我只是没有找到它。

关于第二个用法,Data.List.groupBy first_isnt_newline,其中二元谓词基本上忽略了第二个参数并将一元谓词应用于第一个,我刚刚发现 Data.List.HT.segmentAfter unary_predicate 可以完成这项工作,其中 unary_predicate 是转发const 的输出的一元谓词的否定。换句话说Data.List.groupBy ((/= '\n').) . const === Data.List.HT.segmentAfter (=='\n')

【问题讨论】:

  • "这意味着谓词 eq 在每个组的任何两个元素之间都成立"。严格地说 no,它由组的 first 和组的其余部分持有。
  • 这个想法是你传递一个 equivalence 关系,它是 transitive,所以这意味着如果p x y 成立并且p y z成立,那么p x z 也应该成立。

标签: haskell functional-programming grouping


【解决方案1】:

有一个 groupBy 包可以做到这一点。

但这里有另一种实现方式:

  • 用尾部压缩列表以测试相邻元素的谓词

  • 通过扫描结果并在谓词为假时增加组来生成“组索引”

  • 按索引分组

  • 删除索引

groupByAdjacent :: (a -> a -> Bool) -> [a] -> [[a]]
groupByAdjacent p xs
  = fmap (fmap fst)
  $ groupBy ((==) `on` snd)
  $ zip xs
  $ scanl' (\ g (a, b) -> if p a b then g else succ g) 0
  $ zip xs
  $ drop 1 xs

对于像[1, 2, 3, 10, 11, 20, 30] 这样的输入,谓词将返回[True, True, False, True, False, False],结果组索引将是[0, 0, 0, 1, 1, 2, 3]

扫描也可以写成scanr (bool succ id . uncurry p) 0,因为扫描方向无关紧要(尽管组索引会颠倒)。组索引可能很方便,或者保存为整数更易读,但它可以是Bool,因为组的最小大小是1:扫描的函数参数是bool not id . uncurry p,它可以简化为(==) . uncurry p。这些部分中的一些可以被分解成可重用的函数,比如zipNext = zip <*> drop 1,但为了简单起见,我将它们内联了。

【讨论】:

  • 关于fmap (fmap fst),它有点神秘,我觉得fmap fst <$> 更清楚。但是,接受了,因为它在顶部为我提供了有用的链接,并在最后一段中提供了许多有趣的提示,谢谢!
  • @Enrico:我真的只是想让它适合($) 应用程序链,顺便说一句,如果你愿意的话,它可以更容易地重构为(.) 组合。 TMTOWTDI 虽然:fmap (fmap f) xs = fmap f <$> xs = (f <$>) <$> xs = (fmap . fmap) f xs = f <$$> xs = ...。就个人而言,我实际上喜欢f . … . f n 次的习语,其中f 是某种“映射”函数(fmaptraversefoldMap(=<<)、@ 987654347@, &c.) 来说明 n 层结构下的映射——尽管通常我会在上下文中给它们更多的描述性名称。
猜你喜欢
  • 2021-05-04
  • 1970-01-01
  • 1970-01-01
  • 2022-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多