如何在Haskell中折叠状态?

时间:2014-10-15 19:06:10

标签: list haskell fold traversable foldable

我有一个简单的函数(实际上用于项目Euler的一些问题)。它将一个数字列表转换为十进制数。

fromDigits :: [Int] -> Integer
fromDigits [x] = toInteger x
fromDigits (x:xs) = (toInteger x) * 10 ^ length xs + fromDigits xs

我意识到类型[Int]并不理想。 fromDigits应该可以采取其他输入,例如序列,甚至可能是foldables ...

我的第一个想法是用状态"替换上面的代码"折叠。上述函数的正确(=最小)Haskell类别是什么?

6 个答案:

答案 0 :(得分:6)

首先,折叠已经是关于携带一些状态。 Foldable正是您正在寻找的,不需要State或其他monad。

其次,在空列表中定义基本案例然后非空列表的情况更自然。现在的方式,这个函数在空列表中是未定义的(虽然它完全有效)。请注意[x]只是x : []的缩写。

在当前形式中,使用foldr函数几乎可表达。但是,在foldl范围内,列表或其部分不可用,因此您无法计算length xs。 (在每一步计算length xs也会使整个函数不必要地 O(n ^ 2)。)但是这可以很容易地避免,如果你重新使用过程来使用列表其他方式。函数的新结构可能如下所示:

fromDigits' :: [Int] -> Integer
fromDigits' = f 0
  where
    f s []     = s
    f s (x:xs) = f (s + ...) xs

之后,尝试使用foldl来表达f,最后将其替换为Foldable.foldl

答案 1 :(得分:3)

您应该避免使用length并使用foldl(或foldl')编写您的函数:

fromDigits :: [Int] -> Integer
fromDigits ds = foldl (\s d -> s*10 + (fromIntegral d)) 0 ds

从这个概括到任何可折叠应该是明确的。

答案 2 :(得分:1)

解决此问题的更好方法是建立一个10的权力列表。使用iterate非常简单:

powersOf :: Num a => a -> [a]
powersOf n = iterate (*n) 1

然后你只需要将这些10的幂乘以它们各自的数字列表中的值。这可以通过zipWith (*)轻松完成,但您必须先确保它的顺序正确。这基本上只是意味着您应该重新排序您的数字,以便它们按降序而不是按升序排列:

zipWith (*) (powersOf 10) $ reverse xs

但我们希望它返回Integer,而不是Int,所以让我们通过那里的map fromIntegral

zipWith (*) (powersOf 10) $ map fromIntegral $ reverse xs

剩下的就是总结一下

fromDigits :: [Int] -> Integer
fromDigits xs = sum $ zipWith (*) (powersOf 10) $ map fromIntegral $ reverse xs

或者对于无点球迷来说

fromDigits = sum . zipWith (*) (powersOf 10) . map fromIntegral . reverse

现在,你也可以使用一个折叠,它基本上只是一个纯粹的for循环,其中函数是你的循环体,初始值是,初始状态,你提供它的列表是你的值&# 39;重新循环。在这种情况下,你的州是一个总和,你有什么权力。我们可以使用我们自己的数据类型来表示这个,或者我们可以使用一个元组,第一个元素是当前的总数,第二个元素是当前的幂:

fromDigits xs = fst $ foldr go (0, 1) xs
    where
        go digit (s, power) = (s + digit * power, power * 10)

这大致相当于Python代码

def fromDigits(digits):
    def go(digit, acc):
        s, power = acc
        return (s + digit * power, power * 10)

    state = (0, 1)
    for digit in digits:
        state = go(digit, state)
    return state[0]

答案 3 :(得分:0)

这样一个简单的函数可以在其裸参数中携带其所有状态。继承累加器参数,操作变得微不足道。

fromDigits :: [Int] -> Integer
fromDigits xs = fromDigitsA xs 0  # 0 is the current accumulator value

fromDigitsA [] acc = acc
fromDigitsA (x:xs) acc = fromDigitsA xs (acc * 10 + toInteger x)

答案 4 :(得分:0)

如果你真的决定使用右侧折叠,你可以将length xs计算与这样的计算结合起来(冒昧地定义fromDigits [] = 0):

fromDigits xn = let (x, _) = fromDigits' xn in x where
    fromDigits' [] = (0, 0)
    fromDigits' (x:xn) = (toInteger x * 10 ^ l + y, l + 1) where
        (y, l) = fromDigits' xn

现在很明显这等同于

fromDigits xn = fst $ foldr (\ x (y, l) -> (toInteger x * 10^l + y, l + 1)) (0, 0) xn

当您使用折叠重写递归函数时,向累加器添加额外组件或结果并在折叠返回后丢弃它的模式非常普遍。

话虽如此,foldr的第二个参数总是严格的函数是一个非常非常糟糕的想法(过多的堆栈使用,可能是长列表上的堆栈溢出),你真的应该写{ {1}}作为fromDigits,正如其他一些答案所暗示的那样。

答案 5 :(得分:0)

如果你想“折叠状态”,可能Traversable是你正在寻找的抽象。 Traversable类中定义的方法之一是

traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

基本上,traverse采用类型为a -> f b的“有状态函数”,并将其应用于容器t a中的每个函数,从而生成容器f (t b)。在这里,f可以是State,您可以使用类型为Int -> State Integer ()的函数的遍历。它会构建一个无用的数据结构(在你的情况下单位列表),但你可以丢弃它。以下是使用Traversable解决问题的方法:

import Control.Monad.State
import Data.Traversable

sumDigits :: Traversable t => t Int -> Integer
sumDigits cont = snd $ runState (traverse action cont) 0
  where action x = modify ((+ (fromIntegral x)) . (* 10))

test1 = sumDigits [1, 4, 5, 6]

但是,如果你真的不喜欢构建丢弃的数据结构,你可以使用Foldable实现一些棘手的Monoid:不仅存储计算结果,还存储10^n,其中n是转换为此值的位数。此附加信息使您能够组合两个值:

import Data.Foldable
import Data.Monoid

data Digits = Digits
  { value :: Integer
  , power :: Integer
  }

instance Monoid Digits where
  mempty = Digits 0 1
  (Digits d1 p1) `mappend` (Digits d2 p2) =
    Digits (d1 * p2 + d2) (p1 * p2)

sumDigitsF :: Foldable f => f Int -> Integer
sumDigitsF cont = value $ foldMap (\x -> Digits (fromIntegral x) 10) cont

test2 = sumDigitsF [0, 4, 5, 0, 3]

我坚持第一次实施。虽然它构建了不必要的数据结构,但它更简单易懂(只要读者理解Traversable)。