Haskell中有终止折叠吗?

lzfw57am  于 2024-01-08  发布在  其他
关注(0)|答案(3)|浏览(103)

我需要某种折叠,可以终止,如果我已经有我想要的数据。
例如,我需要找到前3个大于5的数字。我决定使用Either来终止,我的代码看起来像这样:

terminatingFold :: ([b] -> a -> Either [b] [b]) -> [a] -> [b]
    terminatingFold f l = reverse $ either id id $ fold [] l
      where fold acc [] = Right acc
            fold acc (x:xs) = f acc x >>= flip fold xs

    first3NumsGreater5 acc x =
      if length acc >= 3
        then Left acc
        else Right (if x > 5 then (x : acc) else acc)

字符串
有没有更聪明/通用的方法?

a1o7rhls

a1o7rhls1#

函数的结果是一个列表,如果它是惰性生成的,也就是说,从结果中提取一个项只需要评估输入列表,直到在那里找到该项。
Unfolds are under-appreciated。与其关注“消费”输入列表,不如将其视为种子(与内部累加器配对),我们可以逐个元素地从中产生结果。
让我们定义一个Seed类型,它包含一个泛型累加器,与输入中尚未使用的部分配对:

{-# LANGUAGE NamedFieldPuns #-}
import Data.List (unfoldr)

data Seed acc input = Seed {acc :: acc, pending :: [input]}

字符串
现在让我们将first3NumsGreater5重新公式化为一个函数,它要么从Seed中产生下一个输出元素,要么产生没有更多元素的信号:

type Counter = Int

first3NumsGreater5 :: Seed Counter Int -> Maybe (Int, Seed Counter Int)
first3NumsGreater5 (Seed {acc, pending})
  | acc >= 3 =
    Nothing
  | otherwise =
    case dropWhile (<= 5) pending of
      [] -> Nothing
      x : xs -> Just (x, Seed {acc = succ acc, pending = xs})


现在我们的主函数可以用unfoldr来写:

unfoldFromList ::
  (Seed acc input -> Maybe (output, Seed acc input)) ->
  acc ->
  [input] ->
  [output]
unfoldFromList next acc pending = unfoldr next (Seed {acc, pending})


将其付诸实践:

main :: IO ()
main = print $ unfoldFromList first3NumsGreater5 0 [0, 6, 2, 7, 9, 10, 11]
-- [6,7,9]

vsikbqxv

vsikbqxv2#

通常情况下,支持 * 提前终止 * 的文件夹是foldr,其第二个参数的组合函数是非严格的。但是,它的信息流是从右到左的(如果有的话),而你希望它是从左到右的。
一个可能的解决方案是让foldr作为一个 left fold,然后可以让它提前停止:

foldlWhile :: Foldable t 
           => (a -> Bool) -> (r -> a -> r) -> r 
           -> t a -> r
foldlWhile t f a xs  =  foldr cons (\acc -> acc) xs a
  where
    cons x r acc | t x  =  r (f acc x) 
                 | otherwise  =  acc

字符串
您需要对t进行调整,以测试acc而不是x,以满足您的目的。
这个函数是foldlWhilehttps://wiki.haskell.org/Foldl_as_foldr_alternative,重写了一点。foldl'Breaking从那里可能更适合法案。
使用lazy reducer函数的foldr可以像unfoldr一样完美地表达协递归。
而且你的代码已经很懒惰了:terminatingFold (\acc x -> Left acc) [1..] => []。这就是为什么我不确定这个答案是否像你要求的那样“更聪明”。

  • edit:* 在@danidiaz的评论之后,为了使它适当地懒惰,你必须将其编码为例如。
first3above5 :: (Foldable t, Ord a, Num a) 
             => t a -> [a]
first3above5 xs  =  foldr cons (const []) xs 0
   where
   cons x r i | x > 5  =  if i==2 then [x]
                                  else x : r (i+1)
              | otherwise  =  r i


这可以通过抽象测试和计数来进一步推广。
当然,它只是重新实现了take 3 . filter (> 5),但它展示了如何在foldr中实现它。

e1xvtsh3

e1xvtsh33#

如果你使用foldl的一元版本foldM,你所采取的方法就可以工作。使用Either monad,foldM将终止于Left结果,所以我们可以调整你的代码:

import Control.Monad (foldM)

list :: [Int]
list = [7,1,8,2,9,3,10,4]

firstThreeMoreThanFive list = reverse $ either id id $ foldM folder [] list

folder :: [Int] -> Int -> Either [Int] [Int]
folder acc x | (length acc' == 3) = Left acc'
             | otherwise          = Right acc'

  where acc' = if (x > 5) then x:acc else acc

字符串

相关问题