如何在没有手写递归的情况下打破Haskell中的纯循环?

时间:2014-01-20 15:54:20

标签: haskell recursion fold

我想编写一个函数,通过更新累加器的列表,直到累加器达到某个条件或者到达列表的末尾。例如,一个产品函数在其累加器达到零时立即停止。

我知道如何通过手工编写递归来编写代码:

{-# LANGUAGE BangPatterns #-}

prod :: [Integer] -> Integer
prod xs =
    go 1 xs
  where
    go 0   _       = 0
    go !acc []     = acc
    go !acc (x:xs) = go (acc * x) xs

但有没有办法使用折叠和其他更高阶函数对此进行编码?


首先想到的是定义

mult 0 _ = 0
mult x y = x * y

然后使用foldl'。然而,这并没有提前爆发,所以它有点浪费性能。

我们不能使用foldr,因为它以错误的顺序通过列表,并且它的“早期爆发”的方式是通过查看列表的元素而不是查看累加器(这将是重要的,如果累加器的类型不同于列表元素。)

2 个答案:

答案 0 :(得分:18)

一种简单的方法是在允许提前逃避的monad中进行计算,例如EitherMaybe

{-# LANGUAGE BangPatterns #-}
import Data.Functor ((<$))
import Data.Maybe (fromMaybe)
import Control.Monad

prod :: [Integer] -> Integer
prod = fromMaybe 0 . prodM

-- The type could be generalized to any MonadPlus Integer
prodM :: [Integer] -> Maybe Integer
prodM = foldM (\ !acc x -> (acc * x) <$ guard (acc /= 0)) 1

在计算的每一步,我们检查累加器是否非零。如果它为零,guard将调用mplus,它会立即退出计算。例如,以下退出:

> prod ([1..5] ++ [0..])
0

答案 1 :(得分:5)

似乎scanl是最简单的列表组合器,可以为您提供所需的内容。例如,这不会评估第二个列表的110

Prelude> let takeUntil _ [] = []; takeUntil p (x:xs) = if p x then [x] else (x: takeUntil p xs)
Prelude> (last . takeUntil (==0) . scanl (*) 1) ([1..10] ++ [0..10])
0

takeUntil似乎不存在于标准库中。它就像takeWhile,但也为你提供了第一个失败元素。

如果你想要做到这一点,你应该注意部分last。 如果你想要一个强大的通用解决方案,我想mapAccumL是可行的方法。