如何使用State monad计算Haskell中列表中偶数的整数?

时间:2016-10-12 04:34:35

标签: haskell

我试图通过构建我自己的例子来了解Haskell中State Monad的一些基本原理。

考虑一个简单的例子,我想计算整数数组中偶数的整数。当然这可以使用纯函数非常容易地完成,但是我想尝试围绕状态monad路径,在那里我们保持一个计数器,该计数器为已经检查的每个元素保持递增。

这是迄今为止我设法提出的部分(但显然是错误的)尝试。

import Control.Monad.State

f' :: [Int] -> State Int [Int]
f' []     = state (\s -> ([],s)) 
f' (x:xs) = if x `mod` 2 == 0 then state (\s -> ((x:xs), s+1)) -- how can I fix this line? 
            else f' xs

此代码编译,但显然没有给出正确的答案。然后我如何修复此代码,执行类似于以下Python代码的操作

counter = 0 # incremented when we encounter an even integer.
for elt in integer_list:
   if elt % 2 == 0 :
      counter = counter + 1 

2 个答案:

答案 0 :(得分:4)

另一个答案从头开始构建实现。我认为,对现有代码进行微小更改以使其变得合理也是值得的。我们甚至会保留你现有的类型 - 虽然另一个答案提出它会被改变,但我认为这是可以接受的(如果不是很好的话)。

在我看来,真正的问题是你只在if的一个分支中进行了递归。我们真正想要的是递归当前元素是否均匀。所以:

f' (x:xs) = do
    if x `mod` 2 == 0 then state (\s -> ((), s+1)) -- increase the count by one
                      else state (\s -> ((), s  )) -- don't increase the count by one
    rest <- f' xs -- compute the answer for the rest of the list
    return (x:rest) -- reconstruct the answer for the whole list

我们可以检查它在ghci中是否正确:

> runState (f' [1..5]) 0
([1,2,3,4,5],2)

这是您可以做出的最小改变,以使您的实施理念发挥作用。

从那里,我会建议一些重构。首先,您普遍使用state是一种代码气味。我会用这种方式写出各种用法:

f' [] = return []
f' (x:xs) = do
    if x `mod` 2 == 0 then modify (+1) else return ()
    rest <- f' xs
    return (x:rest)

从这里开始,我会在条件中使用even函数,并注意when函数实现&#34;做一些动作或return ()&#34;操作。所以:

f' [] = return []
f' (x:xs) = do
    when (even x) (modify (+1))
    rest <- f' xs
    return (x:rest)

此外,我们实际上有一个组合器,用于对列表的每个元素运行monadic动作;那是mapM。所以我们可以用这种方式将上面的显式递归转换为隐式递归:

f' xs = mapM (\x -> when (even x) (modify (+1)) >> return x) xs

最后,我认为函数返回它消耗的列表有点奇怪。不是 unidiomatic ,本身就像以前的反对意见一样,但可能不是你想要的。如果事实证明您没有计划在任何后续计算中使用结果列表,那么随着时间的推移扔掉它会更有效率;而mapM_组合子就是这样做的。所以:

f' :: [Int] -> State Int ()
f' xs = mapM_ (\x -> when (even x) (modify (+1))) xs

此时,我会认为f'是你提出的想法的一个很好的实现。

答案 1 :(得分:3)

让我们回到绘图板。您想要使用的实际功能类似于

countEven :: [Int] -> Int
countEven xs = runStateMagic xs

其中runStateMagic使用隐藏在其深处的State。这个功能怎么样?好吧,它必须使用execStateevalState。由于我们只对州内感兴趣(也就是我们当前的数字计数),所以让我们用runStateMagic替换execState

countEven :: [Int] -> Int
countEven xs = execState someState 0

现在,execState的类型将stateFunc修复为State Int a。状态的实际值类型是任意的,因为我们无论如何都不会使用它。那么someState应该怎么做?它可能应该在列表上工作,如果我们有偶数,则使用modify' (+1)。让我们为此写一个帮手:

increaseIfEven :: Int -> State Int ()
increaseIfEven n
  | even n    = modify' inc
  | otherwise = return ()

如果数字是偶数,现在将修改状态。我们所要做的就是将其应用于列表中的每个元素。因此,对于列表xs,我们可以简单地执行

mapM_ increaseIfEven xs

请记住,mapM_ :: (a -> m b) -> [a] -> m ()。但在我们的情况下,mState Int,因此它已包含我们的计数器。

总而言之,我们最终得到了

countEven :: [Int] -> Int
countEven xs = execState (mapM_ increaseIfEven xs) 0

但请记住:重要的是修复原始函数的类型f'