暂停单子


68

Monad可以完成许多令人惊奇的疯狂事情。他们可以创建保存值叠加的变量。它们可以允许您在计算数据之前访问将来的数据。它们可以允许您编写破坏性的更新,但实际上不是。然后,延续monad可以让您大开眼界!通常是您自己的。;-)

但这是一个挑战:您可以制作一个可以暂停的单子吗?

数据暂停sx
实例Monad(Pause)
变异::(s-> s)->暂停s()
产量::暂停s()
步骤:: s->暂停s()->(s,也许(暂停s()))

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

要获得更大的声望,请执行以下操作:允许呼叫者修改两次呼叫之间的状态step。(例如,上面的类型签名应允许这样做。)


用例:编写执行复杂功能的代码通常很容易,但是使用一个完整的PITA对其进行转换以在其操作中也输出中间状态。如果您希望用户能够在执行过程中进行某些更改,那么事情就会变得非常复杂。

实施思路:

  • 显然,可以使用线程,锁和来完成IO。但是,我们可以做得更好吗?;-)

  • 延续单子的疯狂吗?

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


3
Cont我想,没有比其他任何例子更疯狂的了。戳callCC
geekosaur 2012年

2
在第一个实例中,我将尝试在签名{mutate ::(s-> s)->(); yield ::()->()}。
Pigworker 2012年

2
GHC有一个monad,您可以恢复(ResumeT),但是由于某种原因,我认为它在6.8版左右消失了。
斯蒂芬·泰特利

在SO问题中看到这么多非常好的答案是很不寻常的。:-D
MathematicalOrchid

1
这是因为您将问题发布为挑战。“单子很棒,但是他们能做X !!?” 社区对此做出了回应:“为什么是!他们可以!!”
罗伯特·马赛奥利

Answers:


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延续,或将其余的计算添加到暂停中。


包括getput因此实现精神以及原始问题的要点的要点:)
Ben Millwood'4

从该实现中很容易看出step可以将其增强为类型签名Pause s x -> s -> (Either x (Pause s x), s)。只需更改行(Done x , s') -> (Left x, s')并更改JustRight下一行。实际上,ParseResult sa ===要么(暂停sa)
丹·伯顿

做得非常好。我忘了我需要get。(我实际上并不需要putmutate但这很简单。)这是一个非常好的答案。
MathematicalOrchid

66

注意:您没有为自己提供直接访问s此monad当前状态的权限。

Pause smutateyield操作上只是一个免费的monad 。直接实现,您将获得:

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单子应用到您现有的免费单子上,或者仅使用“免费教会”单子,这两种方法我都在我的博客中进行了详细介绍。

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编码版本的结果是,您可以轻松推断数据类型的模型,但仍可以得到快速评估模型。


1
这个答案有很多很棒的地方。在哪里可以阅读有关免费monad的更多信息?(我知道“单点数据类型”中有东西,但我正在寻找更多东西)
Alexandre C.

非常好。虽然我更喜欢ehird。我稍后会查看博客参考...
MathematicalOrchid

1
haskell.org/haskellwiki/Free_structure很好地介绍了免费的monad。我在博客上也谈到了很多,但是那里的相关内容更加分散。
爱德华·KMETT


34

我将使用免费的monad来解决这个问题。嗯,那是什么?它们是树,在节点处有>>=作用,在叶子处有值,就像树的嫁接一样起作用。

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代表修改和屈服之间的选择。任何签名简单命令的(根据值与世界进行通信而不是操纵计算)都可以通过这种方式转化为函子。我必须手动做这件事很麻烦!

那给了我您的单子:修改和收益的签名上的免费单子。

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

我可以将Modify和yield命令定义为“先执行后返回”。除了为协商虚拟输入外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函数将运行策略,直到它以Ret或结尾,或者随其状态变化而产生状态。

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


2
我认为公开模式并构建仅用于实例化的工具包可能会很有用。当然,使用(Do(L(f(?:k)))模式非常不愉快。我通常使用“模式同义词”使之更具可读性。遵循这种模式(或更快的细节)应该减少工作量。也许我会做到。
Pigworker 2012年

1
绝对是最抽象的。就我个人而言,我很难遵循它,但是我确信某个地方的人会发现它很有趣。
MathematicalOrchid'4

是否有可能以某种方式减少L(L(LR))的数量?我用5个命令制作了monad,但语法变得有点难以使用(特别是如果我想让monad做10种不同的事情)。 –这是一件小事。我觉得这很有用
埃德加·克莱克斯

2
如上文所述,我通常使用模式同义词来隐藏内容。否则,DeriveFunctor可能足以满足您的目的。有关使用可扩展总和的有用类型类黑客,另请参阅“数据类型的点菜”和“取消嵌入特定领域的语言”。
养猪工人

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

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 s单子是一样的Coroutine (State s) Id。您可以将协程与其他monad结合使用。

相关:http ://themonadreader.files.wordpress.com/2010/01/issue15.pdf中的提示单子


11

注意:此答案可用从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 ()))

概括

我们的Pausemonad目前已由国家参数化。但是,现在我们看到,我们实际上并不需要状态,也不需要使用状态monad的任何细节。因此,我们可以尝试制定一种更通用的解决方案,该解决方案可以通过任何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变压器。此外,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

如果Eitherpart为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-协程

我们实现的是一个实现协程的相当通用的单子解决方案。也许并不奇怪,有人在:-)之前就有了这个主意,并创建了monad-协程包。毫不奇怪,它与我们创建的内容非常相似。

该软件包进一步推广了该想法。连续的计算存储在任意函子中。这允许挂起许多变化,以处理挂起的计算。例如,将值传递给resume的调用者(我们称为step),或等待提供的值继续使用,等等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.