State monad 乍一看确实有点令人困惑。让我们按照 Norman Ramsey 的建议进行操作,并逐步了解如何从头开始实施。警告,这很长!
首先,State 有两个类型参数:包含的状态数据的类型和计算的最终结果的类型。我们将在这里分别使用stateData
和result
作为它们的类型变量。如果您考虑一下,这是有道理的;基于状态的计算的定义特征是它在产生输出的同时修改状态。
不太明显的是,类型构造函数将函数从状态转换为修改后的状态和结果,如下所示:
newtype State stateData result = State (stateData -> (result, stateData))
因此,虽然 monad 被称为“State”,但 monad 包装的实际值是基于 State 的计算的值,而不是包含状态的实际值。
记住这一点,我们不应该惊讶地发现runState
用于在 State monad 中执行计算的函数实际上只不过是包装函数本身的访问器,可以这样定义:
runState (State f) = f
那么当你定义一个返回 State 值的函数时,这意味着什么呢?让我们暂时忽略 State 是一个 monad 的事实,只看底层的类型。首先,考虑这个函数(它实际上并没有对状态做任何事情):
len2State :: String -> State Int Bool
len2State s = return ((length s) == 2)
如果你看State的定义,我们可以看到这里的stateData
类型是Int
,result
类型是Bool
,所以数据构造函数包装的函数必须是类型Int -> (Bool, Int)
。现在,想象一个无状态版本的 --len2State
显然,它会有 type String -> Bool
。那么,您将如何将这样的函数转换为返回适合状态包装器的值的函数呢?
好吧,很明显,转换后的函数将需要第二个参数,一个Int
代表状态值的参数。它还需要返回一个状态值 another Int
。因为我们实际上并没有对这个函数中的状态做任何事情,所以让我们做一件显而易见的事情——直接传递那个 int。这是一个状态形函数,根据无状态版本定义:
len2 :: String -> Bool
len2 s = ((length s) == 2)
len2State :: String -> (Int -> (Bool, Int))
len2State s i = (len2' s, i)
但这有点愚蠢和多余。让我们对转换进行概括,以便我们可以传入结果值,并将任何内容转换为类似 State 的函数。
convert :: Bool -> (Int -> (Bool, Int))
convert r d = (r, d)
len2 s = ((length s) == 2)
len2State :: String -> (Int -> (Bool, Int))
len2State s = convert (len2 s)
如果我们想要一个改变状态的函数怎么办?显然我们不能用 构建一个convert
,因为我们写它是为了传递状态。让我们保持简单,并编写一个函数来用新值覆盖状态。它需要什么样的类型?它需要一个Int
新的状态值,当然必须返回一个函数stateData -> (result, stateData)
,因为这是我们的状态包装器需要的。覆盖状态值在状态计算之外并没有真正有意义的result
值,所以我们的结果将只是()
,在 Haskell 中表示“无值”的零元素元组。
overwriteState :: Int -> (Int -> ((), Int))
overwriteState newState _ = ((), newState)
那很简单!现在,让我们对这些状态数据进行实际操作。让我们从上面重写len2State
一些更明智的东西:我们将字符串长度与当前状态值进行比较。
lenState :: String -> (Int -> (Bool, Int))
lenState s i = ((length s) == i, i)
我们可以像以前那样将其概括为转换器和无状态函数吗?没那么容易。我们的len
函数需要将状态作为参数,但我们不希望它“知道”状态。确实很尴尬。但是,我们可以编写一个快速的辅助函数来为我们处理所有事情:我们将给它一个需要使用状态值的函数,它会将值传入,然后将所有内容打包成一个状态形函数不留len
任何智慧。
useState :: (Int -> Bool) -> Int -> (Bool, Int)
useState f d = (f d, d)
len :: String -> Int -> Bool
len s i = (length s) == i
lenState :: String -> (Int -> (Bool, Int))
lenState s = useState (len s)
现在,棘手的部分——如果我们想将这些函数串在一起怎么办?假设我们想lenState
在一个字符串上使用,如果结果为假,则将状态值加倍,然后再次检查字符串,如果任何一个检查成功,最后返回真。我们拥有完成这项任务所需的所有部分,但全部写出来会很痛苦。我们可以创建一个函数,自动将两个函数链接在一起,每个函数都返回类似状态的函数吗?确定的事!我们只需要确保它接受两件事作为参数:第一个函数返回的 State 函数,以及一个将前一个函数的result
类型作为参数的函数。让我们看看结果如何:
chainStates :: (Int -> (result1, Int)) -> (result1 -> (Int -> (result2, Int))) -> (Int -> (result2, Int))
chainStates prev f d = let (r, d') = prev d
in f r d'
所有这一切都是将第一个状态函数应用于一些状态数据,然后将第二个函数应用于结果和修改后的状态数据。很简单,对吧?
现在,有趣的部分:在chainStates
和之间convert
,我们应该几乎可以将无状态函数的任意组合变成启用状态的函数!我们现在唯一需要的是一个替换,useState
它返回状态数据作为结果,以便chainStates 可以将它传递给对我们正在使用的技巧一无所知的函数。此外,我们将使用 lambdas 来接受前面函数的结果并给它们临时名称。好的,让我们实现这一点:
extractState :: Int -> (Int, Int)
extractState d = (d, d)
chained :: String -> (Int -> (Bool, Int))
chained str = chainStates extractState $ \state1 ->
let check1 = (len str state1) in
chainStates (overwriteState (
if check1
then state1
else state1 * 2)) $ \ _ ->
chainStates extractState $ \state2 ->
let check2 = (len str state2) in
convert (check1 || check2)
并尝试一下:
> chained "abcd" 2
(True, 4)
> chained "abcd" 3
(False, 6)
> chained "abcd" 4
(True, 4)
> chained "abcdef" 5
(False, 10)
当然,我们不能忘记 State 实际上是一个 monad,它封装了类似 State 的函数并让我们远离它们,所以我们构建的任何漂亮函数都无法帮助我们处理真实的事情。或者他们会?令人震惊的是,事实证明,真正的 State monad 提供了所有相同的功能,但名称不同:
runState (State s) = s
return r = State (convert r)
(>>=) s f = State (\d -> let (r, d') = (runState s) d in
runState (f r) d')
get = State extractState
put d = State (overwriteState d)
请注意,>>= 几乎与chainStates 相同,但没有使用chainStates 定义它的好方法。因此,总结一下,我们可以使用真实状态重写最后一个示例:
chained str = get >>= \state1 ->
let check1 = (len str state1) in
put (if check1
then state1 else state1 * 2) >>= \ _ ->
get >>= \state2 ->
let check2 = (len str state2) in
return (check1 || check2)
或者,所有都使用等效的 do 表示法:
chained str = do
state1 <- get
let check1 = len str state1
_ <- put (if check1 then state1 else state1 * 2)
state2 <- get
let check2 = (len str state2)
return (check1 || check2)