注意:此答案可用从Gist的Haskell识字文件中获得。
我很喜欢这个练习。我试着不看答案就做,这是值得的。我花了相当长的时间,但结果出奇地接近其他两个答案以及monad-coroutine库。因此,我认为这是解决该问题的自然方法。没有这项练习,我将无法理解monad-coroutine的工作原理。
为了增加一些附加值,我将解释最终导致我找到解决方案的步骤。
识别状态单子
由于我们正在处理状态,因此我们在寻找可以由状态monad有效描述的模式。特别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的任何细节。因此,我们可以尝试制定一种更通用的解决方案,该解决方案可以通过任何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变压器。此外,mutate
的签名与从的抬升完全相同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
part为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 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-协程
我们实现的是一个实现协程的相当通用的单子解决方案。也许并不奇怪,有人在:-)之前就有了这个主意,并创建了monad-协程包。毫不奇怪,它与我们创建的内容非常相似。
该软件包进一步推广了该想法。连续的计算存储在任意函子中。这允许挂起许多变化,以处理挂起的计算。例如,将值传递给resume的调用者(我们称为step
),或等待提供的值继续使用,等等。
Cont
我想,没有比其他任何例子更疯狂的了。戳callCC
。