我试图通过构建我自己的例子来了解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
答案 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
。这个功能怎么样?好吧,它必须使用execState
或evalState
。由于我们只对州内感兴趣(也就是我们当前的数字计数),所以让我们用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 ()
。但在我们的情况下,m
为State Int
,因此它已包含我们的计数器。
总而言之,我们最终得到了
countEven :: [Int] -> Int
countEven xs = execState (mapM_ increaseIfEven xs) 0
但请记住:重要的是修复原始函数的类型f'
。