72

Monads 可以做许多令人惊奇、疯狂的事情。他们可以创建包含值叠加的变量。它们可以让您在计算之前访问未来的数据。它们可以让你编写破坏性的更新,但不是真的。然后延续单子让你打破人们的想法!通常是你自己的。;-)

但这里有一个挑战:你能制作一个可以暂停的 monad吗?

数据暂停 sx
实例 Monad (Pause s)
变异 :: (s -> s) -> 暂停 s ()
产量::暂停s()
step :: s -> Pause s () -> (s, Maybe (Pause s ()))

Pausemonad 是一种状态 monad(因此,mutate具有明显的语义)。通常,像这样的 monad 具有某种“运行”功能,它运行计算并将最终状态返回给您。但Pause不同的是:它提供了一个step函数,该函数运行计算,直到它调用神奇的yield函数。此处计算暂停,向调用者返回足够的信息以便稍后恢复计算。

对于额外的 awesomne​​ss:允许调用者修改step调用之间的状态。(例如,上面的类型签名应该允许这样做。)


用例:编写执行复杂操作的代码通常很容易,但需要一个完整的 PITA 来将其转换为还输出其操作中的中间状态。如果您希望用户能够在执行过程中更改某些内容,那么事情会变得非常复杂。

实施思路:

  • 显然,它可以通过线程、锁和IO. 但我们能做得更好吗?;-)

  • 延续单子有些疯狂吗?

  • 也许是某种 writer monad,yield只记录当前状态,然后我们可以step通过迭代日志中的状态来“假装”它。(显然这排除了改变步骤之间的状态,因为我们现在并没有真正“暂停”任何东西。)

4

6 回答 6

68

注意:你没有让自己直接访问s这个 monad 中的当前状态。

Pause s只是mutateandyield操作上的一个自由单子。直接实现你得到:

data Pause s a
  = Return a
  | Mutate (s -> s) (Pause s a)
  | Yield (Pause s a)

instance Monad (Pause s) where
  return = Return
  Return a   >>= k = k a
  Mutate f p >>= k = Mutate f (p >>= k)
  Yield p    >>= k = Yield (p >>= k)

使用几个智能构造函数为您提供所需的 API:

mutate :: (s -> s) -> Pause s ()
mutate f = Mutate f (return ())

yield :: Pause s ()
yield = Yield (return ())

以及驱动它的阶跃函数

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Mutate f k) = step (f s) k
step s (Return ()) = (s, Nothing)
step s (Yield k) = (s, Just k)

您也可以直接使用

data Free f a = Pure a | Free (f (Free f a))

(来自我的“免费”包)

data Op s a = Mutate (s -> s) a | Yield a

那么我们已经有一个暂停的单子

type Pause s = Free (Op s)

并且只需要定义智能构造函数和步进器。

让它更快。

现在,这些实现很容易推理,但它们没有最快的操作模型。特别是,(>>=) 的左关联使用会产生渐近较慢的代码。

为了解决这个问题,您可以将Codensity monad 应用到您现有的免费 monad,或者直接使用“Church free” monad,我在我的博客中对此进行了深入描述。

http://comonad.com/reader/2011/free-monads-for-less/

http://comonad.com/reader/2011/free-monads-for-less-2/

http://comonad.com/reader/2011/free-monads-for-less-3/

应用 Free monad 的 Church 编码版本的结果是,您可以轻松推断数据类型的模型,并且您仍然可以获得快速评估模型。

于 2012-04-19T21:51:03.083 回答
60

当然; 您只需让任何计算以结果结束,或暂停自身,提供要在恢复时使用的操作以及当时的状态:

data Pause s a = Pause { runPause :: s -> (PauseResult s a, s) }

data PauseResult s a
    = Done a
    | Suspend (Pause s a)

instance Monad (Pause s) where
    return a = Pause (\s -> (Done a, s))
    m >>= k = Pause $ \s ->
        case runPause m s of
            (Done a, s') -> runPause (k a) s'
            (Suspend m', s') -> (Suspend (m' >>= k), s')

get :: Pause s s
get = Pause (\s -> (Done s, s))

put :: s -> Pause s ()
put s = Pause (\_ -> (Done (), s))

yield :: Pause s ()
yield = Pause (\s -> (Suspend (return ()), s))

step :: Pause s () -> s -> (Maybe (Pause s ()), s)
step m s =
    case runPause m s of
        (Done _, s') -> (Nothing, s')
        (Suspend m', s') -> (Just m', s')

Monad实例只是以正常方式对事物进行排序,将最终结果传递给k延续,或者添加要在暂停时完成的其余计算。

于 2012-04-19T21:46:57.017 回答
34

这就是我将如何去做,使用免费的单子。呃,嗯,他们是什么?它们是在节点处具有动作,在叶子处具有值的树,其>>=行为类似于树嫁接。

data f :^* x
  = Ret x
  | Do (f (f :^* x))

在数学中为这样的事情写 F * X并不罕见,因此我的胡思乱想中缀类型名称。要创建一个实例,您只需要f成为可以映射的对象:任何Functor人都可以。

instance Functor f => Monad ((:^*) f) where
  return = Ret
  Ret x  >>= k  = k x
  Do ffx >>= k  = Do (fmap (>>= k) ffx)

那只是“k在所有的叶子上应用并嫁接在由此产生的树木中”。这些罐头树代表了交互计算的策略:整棵树覆盖了与环境的所有可能交互,环境选择树中的哪条路径。为什么他们是免费的?它们只是树,没有有趣的方程理论,说明哪些策略等同于其他策略。

现在让我们有一个工具包来制作对应于我们可能希望能够执行的一堆命令的 Functor。这东西

data (:>>:) s t x = s :? (t -> x)

instance Functor (s :>>: t) where
  fmap k (s :? f) = s :? (k . f)

捕获在一个具有输入类型和输出类型的命令x之后获取值的想法。为此,您需要在 中选择一个输入并解释如何继续在给定命令输出中的值。要将函数映射到这样的事物上,请将其附加到延续上。到目前为止,标准设备。对于我们的问题,我们现在可以定义两个函子:stsxt

type Modify s  = (s -> s) :>>: ()
type Yield     = () :>>: ()

就像我刚刚写下我们希望能够执行的命令的值类型一样!

现在让我们确保我们可以在这些命令之间提供选择。我们可以证明在函子之间进行选择会产生一个函子。更多标准设备。

data (:+:) f g x = L (f x) | R (g x)

instance (Functor f, Functor g) => Functor (f :+: g) where
  fmap k (L fx) = L (fmap k fx)
  fmap k (R gx) = R (fmap k gx)

因此,Modify s :+: Yield代表修​​改和屈服之间的选择。任何简单命令的签名(根据值与世界通信,而不是操纵计算)都可以通过这种方式转换为函子。我必须手动完成,这很麻烦!

这给了我你的 monad:在 modify 和 yield 的签名上的自由 monad。

type Pause s = (:^*) (Modify s :+: Yield)

我可以将 modify 和 yield 命令定义为 one-do-then-return。除了协商 的虚拟输入之外yield,这只是机械的。

mutate :: (s -> s) -> Pause s ()
mutate f = Do (L (f :? Ret))

yield :: Pause s ()
yield = Do (R (() :? Ret))

然后该step函数为策略树赋予意义。它是一个控制运算符,从另一个(可能)构造一个计算。

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Ret ())            = (s, Nothing)
step s (Do (L (f  :? k)))  = step (f s) (k ())
step s (Do (R (() :? k)))  = (s, Just (k ()))

step函数运行该策略,直到它以 a 结束Ret,或者它产生,随着它的变化而改变状态。

一般的方法是这样的:将命令(根据值进行交互)与控制运算符(操纵计算)分开;在命令签名上构建“策略树”的自由单子(摇动手柄);通过在策略树上递归来实现控制运算符。

于 2012-04-19T22:15:53.093 回答
14

与您的类型签名不完全匹配,但肯定很简单:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
import Control.Monad.State

newtype ContinuableT m a = Continuable { runContinuable :: m (Either a (ContinuableT m a)) }
instance Monad m => Monad (ContinuableT m) where
    return = Continuable . return . Left
    Continuable m >>= f = Continuable $ do
        v <- m
        case v of
            Left  a -> runContinuable (f a)
            Right b -> return (Right (b >>= f))

instance MonadTrans ContinuableT where
    lift m = Continuable (liftM Left m)

instance MonadState s m => MonadState s (ContinuableT m) where
    get = lift get
    put = lift . put

yield :: Monad m => ContinuableT m a -> ContinuableT m a
yield = Continuable . return . Right

step :: ContinuableT (State s) a -> s -> (Either a (ContinuableT (State s) a), s)
step = runState . runContinuable

-- mutate unnecessary, just use modify
于 2012-04-20T02:13:25.283 回答
11
{-# LANGUAGE TupleSections #-}
newtype Pause s x = Pause (s -> (s, Either x (Pause s x)))

instance Monad (Pause s) where
  return x = Pause (, Left x)

  Pause k >>= f = Pause $ \s -> let (s', v) = k s in
                                case v of
                                  Left x -> step (f x) s'
                                  Right x -> (s', Right (x >>= f))

mutate :: (s -> s) -> Pause s ()
mutate f = Pause (\s -> (f s, Left ()))

yield :: Pause s ()
yield = Pause (, Right (return ()))

step :: Pause s x -> s -> (s, Either x (Pause s x))
step (Pause x) = x

我就是这样写的。我给出了step一个更笼统的定义,它也可以命名为runPause. 事实上,考虑类型step导致我定义Pause.

monad-coroutine包中,你会找到一个通用的 monad 转换器。Pause smonad与Coroutine (State s) Id. 您可以将协程与其他 monad 结合使用。

相关:http ://themonadreader.files.wordpress.com/2010/01/issue15.pdf 中的 Prompt monad

于 2012-04-19T21:50:39.467 回答
11

注意:此答案作为 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 ()))

概括

我们的Pausemonad 目前由状态参数化。然而,现在我们看到我们并不真正需要状态来做任何事情,也没有使用状态单子的任何细节。所以我们可以尝试制作一个由任何 monad 参数化的更通用的解决方案:

mutate :: (Monad m) =    m () -> Pause m ()
yield  :: (Monad m) =    Pause m ()
step   :: (Monad m) =    Pause m () -> m (Maybe (Pause m ()))

此外,我们可以尝试通过允许任何类型的值来制作mutatestep通用的值,而不仅仅是(). 通过意识到这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),或者等待提供的值继续,等等。

于 2012-08-31T13:06:31.120 回答