注意:此答案可作为 Gist 的读写 Haskell 文件获得。
我非常喜欢这个练习。我试图在不看答案的情况下做到这一点,这是值得的。我花了相当长的时间,但结果出乎意料地接近其他两个答案,以及monad-coroutine库。所以我想这是这个问题的自然解决方案。如果没有这个练习,我将无法理解monad-coroutine 的真正工作原理。
为了增加一些额外的价值,我将解释最终导致我找到解决方案的步骤。
认识状态单子
由于我们正在处理状态,因此我们寻找可以由状态单子有效描述的模式。特别s - s
是 与 同构s -> (s, ())
,因此可以将其替换为State s ()
。并且函数类型s -> x -> (s, y)
可以翻转到x -> (s -> (s, y))
,其实就是x -> State s y
。这导致我们更新签名
mutate :: State s () - Pause s ()
step :: Pause s () - State s (Maybe (Pause s ()))
概括
我们的Pause
monad 目前由状态参数化。然而,现在我们看到我们并不真正需要状态来做任何事情,也没有使用状态单子的任何细节。所以我们可以尝试制作一个由任何 monad 参数化的更通用的解决方案:
mutate :: (Monad m) = m () -> Pause m ()
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m () -> m (Maybe (Pause m ()))
此外,我们可以尝试通过允许任何类型的值来制作mutate
更step
通用的值,而不仅仅是()
. 通过意识到这Maybe a
是同构的,Either a ()
我们最终可以将我们的签名推广到
mutate :: (Monad m) = m a -> Pause m a
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m a -> m (Either (Pause m a) a)
以便step
返回计算的中间值。
单子变压器
现在,我们看到我们实际上是在尝试从一个 monad 中创建一个 monad——添加一些额外的功能。这就是通常所说的monad 转换器。此外,的签名与lift frommutate
完全相同。最有可能的是,我们走在正确的轨道上。MonadTrans
最后的单子
该step
函数似乎是我们 monad 中最重要的部分,它定义了我们需要什么。也许,这可能是新的数据结构?我们试试看:
import Control.Monad
import Control.Monad.Cont
import Control.Monad.State
import Control.Monad.Trans
data Pause m a
= Pause { step :: m (Either (Pause m a) a) }
如果该Either
部分是Right
,它只是一个单子值,没有任何暂停。这引导我们如何实现最简单的事情 -lift
函数来自MonadTrans
:
instance MonadTrans Pause where
lift k = Pause (liftM Right k)
并且mutate
只是一个专业:
mutate :: (Monad m) => m () -> Pause m ()
mutate = lift
如果该Either
部分是Left
,则表示暂停后继续计算。所以让我们为此创建一个函数:
suspend :: (Monad m) => Pause m a -> Pause m a
suspend = Pause . return . Left
现在yield
计算很简单,我们只需挂起一个空计算:
yield :: (Monad m) => Pause m ()
yield = suspend (return ())
尽管如此,我们还是错过了最重要的部分。Monad
实例。让我们修复它。实现return
很简单,我们只需要提升内部 monad。实现>>=
有点棘手。如果原始Pause
值只是一个简单的值 ( Right y
),那么我们只需将其包装f y
为结果。如果它是一个可以继续的暂停计算(Left p
),我们递归地进入它。
instance (Monad m) => Monad (Pause m) where
return x = lift (return x) -- Pause (return (Right x))
(Pause s) >>= f
= Pause $ s >>= \x -> case x of
Right y -> step (f y)
Left p -> return (Left (p >>= f))
测试
让我们尝试创建一些使用和更新状态的模型函数,在计算中产生:
test1 :: Int -> Pause (State Int) Int
test1 y = do
x <- lift get
lift $ put (x * 2)
yield
return (y + x)
还有一个调试 monad 的辅助函数 - 将其中间步骤打印到控制台:
debug :: Show s => s -> Pause (State s) a -> IO (s, a)
debug s p = case runState (step p) s of
(Left next, s') -> print s' >> debug s' next
(Right r, s') -> return (s', r)
main :: IO ()
main = do
debug 1000 (test1 1 >>= test1 >>= test1) >>= print
结果是
2000
4000
8000
(8000,7001)
正如预期的那样。
协程和 monad-协程
我们实现的是一个非常通用的单子解决方案,它实现了 Coroutines。也许并不奇怪,之前有人有这个想法:-),并创建了monad-coroutine包。不那么令人惊讶的是,它与我们创建的非常相似。
该软件包进一步概括了这个想法。继续计算存储在任意函子中。这允许挂起许多变体如何处理挂起的计算。例如,将值传递给resume的调用者(我们称之为step
),或者等待提供的值继续,等等。