在 Haskell 中有条件折叠列表的简洁语法 发生了什么

问题描述

我对 haskell 比较陌生,但在我的搜索中,我找不到一种简单的方法来有条件地折叠列表。即当一个元素满足一个条件(如 filter 中)时,通过函数(如 foldrfoldl)折叠该元素。

我的解决方法是编写以下辅助函数,然后根据我的情况应用 map 更改结果对列表。

-- This function returns tuples containing the elements which 
--    satisfy `cond` folded right,adding 1 to the second value
-- in each pair. (`snd` pair starts at 0)
-- Condition takes a single value (similar to `filter`)
-- NOTE: list cannot end with token
foldrOn cond list = 
    if (length list) > 0 then 
        if cond (head list) then 
            do
                let tmp = foldrOn cond (tail list)
                (fst (head tmp),snd (head tmp) + 1) : (tail tmp) 
                      -- fold token into char after it 
        else
            (head list,0) : (foldrOn cond (tail list)) 
                      -- don't fold token
    else
        [] -- base case len list = 0

foldlOn cond list = ...

例如,用例类似于想要删除以下列表中的零,但要记住每个值之间删除了多少个。

-- the second value in each resultant pair represents the number of 
-- zeroes preceding the corresponding first value in the original list.
foldrOn (== 0) [1,1,1] -- [(1,0),(1,5),3)]
foldrOn (== 0) [1,12,13] -- [(1,(12,2),(13,1)]

有没有更好的方法来实现这一点? 此外,这是否可以更优化地完成?

解决方法

首先,

SUBSTRING

这是最简单的,虽然它是递归的,即会在开始返回结果之前强制整个列表结束。

为了让它最大限度地懒惰,我们必须使用一个懒惰的左折叠。跳过满足 foldrOn :: Num t => (a -> Bool) -> [a] -> [(a,t)] -- foldrOn (== 0) [1,1,1] -- [(1,0),(1,5),3)] foldrOn p xs = foldr g [] xs where g x [] = [(x,0)] g x ((y,n):r) | p x = ((y,n+1):r) g x r = ((x,0):r) 的元素仍然是一个递归步骤,但至少该过程会在每个跨度之间暂停。

懒惰左折叠通常实现为 p,附加参数沿列表从左到右传递:

foldr

或者你可以结合 foldlOn :: Num t => (a -> Bool) -> [a] -> [(a,t)] -- foldlOn (== 0) [1,3)] foldlOn p xs = foldr g z xs 0 where g x r i | p x = r (i+1) | otherwise = (x,i) : r 0 z _i = [] /spanbreak 来做同样的事情。

您可能会找到一种将 unfoldr 与一些后处理步骤结合使用的方法:

groupBy

完成这应该不是问题。

,

你总是可以带一些内置机器。 Data.List 库非常强大:

import Data.List(mapAccumL)
import Data.Maybe(catMaybes)

foldrOn cond = catMaybes . snd . mapAccumL combine 0 where
  combine a el =
    if cond el then (a + 1,Nothing)
    else (0,Just (el,a))

发生了什么

本质上,foldrOn cond 是以下函数的组合:

  • mapAccumL combine 0 沿着列表前进,通过有关最近跳过的实体数量的信息修改每个元素(从 0 开始计数,并在我们发现与 {{ 1}} 谓词)。
  • condsnd 的结果中丢弃最终状态
  • mapAccumL 删除 catMaybes 层并只留下“当前”值。
,

让我们从使用模式匹配开始,让您自己的实现更惯用、更明显正确,而且(更快)。我们还可以以惯用的方式使用 guards 而不是 if/then/else;这一点不那么重要。在这里也没有理由使用 do,所以我们不会。

foldrOn _cond [] = []
foldrOn cond (hd : tl)
  | cond hd
  = case foldrOn cond tl of
      (x,y) : tl' -> (x,y + 1) : tl' 
                      -- fold token into char after it
      [] -> error "String ended on token."
  | otherwise
  = (hd,0) : foldrOn cond tl
                      -- don't fold token

这……好吧。但是正如 Will Ness 所建议的,通过将“不完整”元素放入结果列表中,我们实际上并没有得到任何好处。相反,我们可以对满足 cond 的标记进行计数,直到到达块的末尾,然后生成一个完整的元素。我认为这使代码更容易理解,并且它也应该运行得更快一些。

foldrOn cond = go 0
  where
    go count (hd : tl)
      | cond hd
      = go (count + 1) tl -- Don't produce anything; just bump the count
      | otherwise
      = (hd,count) : go 0 tl -- Produce the element and the count; reset the count to 0
    go count []
      | count == 0
      = []
      | otherwise
      = error "List ended on a token."

实际上运行得更快,您可能需要明确告诉编译器您确实想要计算计数。你可能暂时不需要理解这部分,但它看起来像这样:

-- At the top of the file,add this line:
{-# LANGUAGE BangPatterns #-}

foldrOn cond = go 0
  where
    go !count (hd : tl)
      | cond hd
      = go (count + 1) tl -- Don't produce anything; just bump the count
      | otherwise
      = (hd,count) : go 0 tl -- Produce the element and the count; reset the count to 0
    go count []
      | count == 0
      = []
      | otherwise
      = error "List ended on a token."

这可以用 Will Ness 演示的方式写成折叠。

注意:虽然可以避免使用 BangPatterns 语言扩展,但这样做有点烦人。