首先,在学习过程的这一点上,您可能不应该担心“副作用”。此外,您还试图混合两个 monad,State 和 IO,但您似乎也没有掌握。因此,您可能应该更轻松一些。
可以使用IORefs 在IO monad 中执行有状态的操作,您可以将其视为可变变量。如果我是你,我还不会去那里。然后是State monad,粗略地说,它是一种在纯设置中模拟有状态函数的便捷方式。
理论上,您可以将有状态函数f :: a -> b 视为f :: (a,s) -> (b,s) 类型的纯函数,其中s 表示您可以访问和更改的某些状态。以上内容不太适合 monad 框架,因为在 monad m 中,我们希望 a -> m b 表示从 a 到 b 的有效函数。但是很容易适应。类型(a,s) -> (b,s)可以不柯里化得到a -> s -> (b,s),我们把m b取为s -> (b,s),所以a -> m b代表a -> s -> (b,s)。
所以这就是 monad State s 所代表的。对于每个类型b,类型State s b 是s -> (b,s),可以理解为“给我缺少的初始状态s,这样我就可以计算b 和最终状态s。还有一个有状态函数a -> State s b 是a -> s -> (b,s),可以理解为“此函数接受a 并产生一个计算,给定初始状态s 产生结果b 和最终状态s。
这只是为了让您大致了解它的工作原理。现在这里有一些代码可以满足您的需求。让我们从一个简单的方法开始。
size :: [Int] -> State Int ()
size [] = put 0
size (x:xs) = do size xs
num <- get
put (num + 1)
类型是State Int (),因为您只是在更新一个整数状态并且不返回任何值(我们只关心状态)。
该过程与计算大小的常用递归函数非常相似(没有累加器),但我们通过更新状态来完成工作。要运行此示例,只需执行以下操作,
runState (size list) 0
对于一些list。请注意,初始状态 0 在这里无关紧要,因为该算法通过将空列表的状态设置为 0 然后为每个元素添加 1 来工作。
现在是一个以累积方式运行的版本,
sizeAc :: [Int] -> State Int ()
sizeAc [] = return ()
sizeAc (x:xs) = do num <- get
put (num + 1)
sizeAc xs
再次运行这个例子,
runState (sizeAc list) 0
请注意,在这种情况下,您必须使用 0 作为初始状态。该函数所做的是,对于列表中的每个元素,它通过将状态值加一来更新状态。对于空列表,它什么也不做。
最后是带有map 的版本,因为它出现在您最初的尝试中。首先我们实现计数动作。
count :: State Int ()
count = do num <- get
put (num + 1)
此操作包括访问状态并使用添加的单元对其进行更新。然后为列表中的每个元素构建一个此类操作的列表。
sizeAux' :: [Int] -> [State Int ()]
sizeAux' xs = map (\x -> count) xs
注意结果的类型是一个列表。结果是一个列表,其中所有元素都是操作count。然后我们使用sequence_按顺序执行这些动作,其类型如下(专门用于列表和我们特定的monad)。
sequence_ :: [m a] -> m ()
sequence_ :: [State Int ()] -> State Int ()
得到的函数是
size' :: [Int] -> State Int ()
size' xs = sequence_ (sizeAux' xs)
同样可以通过它运行,
runState (size' list) 0
在这里再次注意初始状态0 是必不可少的。
在这一点上,这可能仍然有些复杂。您需要更好地理解 monad 类、do 表示法和 State monad 的特殊性。无论如何,这是您应该前进的方向,而不是将 State 与 IO 混为一谈。