将IO作为MonadCont的实例是否有意义?

时间:2013-11-08 04:59:42

标签: haskell monads continuation

由于MonadContMonad显然比callCC更受限制并提供更多权力。这意味着它的实例越来越少,你可以用它做更多的事情。

查看MonadCont的已定义实例时,看起来列出的所有内容都需要ContContT或已存在的MonadCont实例。这意味着我们必须从一些ContContT开始,特别是无法将IO转变为MonadCont

但是,我认为在callCC上下文中使用IO是有意义的,因此我们可以简化以下内容(根据official Hackage page callCC示例进行调整):< / p>

whatsYourName :: IO ()
whatsYourName = do
    name <- getLine
    let response = flip runCont id $ do
        callCC $ \exit -> do
            when (null name) (exit "You forgot to tell me your name!")
            return $ "Welcome, " ++ name ++ "!"            
    print response

whatsYourName' :: IO ()
whatsYourName' = do
    name <- getLine
    response <- callCC $ \exit -> do
            when (null name) (exit "You forgot to tell me your name!")
            return $ "Welcome, " ++ name ++ "!"            
    print response

以更干净的方式在do块中使用callCC

当然,要使IO成为MonadCont的实例,我们必须有一些魔力,因为callCC IO意味着“使用未来计算调用给定函数指定在现实世界中接下来会发生什么“,所以只有解释器或编译器才能真正知道这意味着什么。另一方面,我没有看到任何理论上的原因,这是可以导入的,因为Scheme已经有很长一段时间了,并且制作这样的实例根本不需要语言更改。

可能的问题

我能想到的一个因素是callCC的语义与正确的清理保证相冲突。许多语言提供了“try ... finally”控件以进行适当的清理,而C ++的析构函数也保证了这一点。我不确定Haskell中的内容是什么,但是如果callCC可用于IO,则可以使用它来逃避任何需要清理的IO涉及的上下文,从而提供保证将会变得不可能,你可以看到what happens in Ruby

讨论意见

@jozefg的回答非常好。我只想在这里写下我的意见。

  1. MonadCont确实来自mtl。但这并不意味着GHC或其他编译器无法定义unsafeCallCC并定义实例,如果具有正确定义的MonadCont在编译模块的范围内并且-XIOMonadCont被设置。

  2. 我已经谈过异常安全问题,看起来很难确定。但是,在我看来,Haskell已经有unsafePerformIO,基本上比unsafeCallCC更不安全。

  3. 原因callCC在大多数情况下过于强大,应尽可能避免。但是,在我看来,继续传递样式可以用来使惰性求值显式化,这有助于更好地理解程序,从而更容易找到可能的优化。因为CPS不是MonadCont,但是使用它并将深层嵌套的内部函数转换为符号是自然的步骤。

4 个答案:

答案 0 :(得分:9)

我说这是一个坏主意。首先,MonadCont位于MTL中。 GHC对此一无所知,这意味着让编译器依赖于第三方库,ick。

其次,callCC甚至在一些非常高调的Schemer中也不受欢迎,主要是因为它使代码的推理成为一种痛苦!与goto难以推理的方式大致相同。特别是在Haskell,我们不得不关注

  1. 是否安全? (这已经很难了)
  2. callCC安全吗?
  3. 最后,我们甚至不需要它。如果您想使用延续和IO,请使用ContT IO,它的功能非常强大。但是,我几乎可以保证它可以用不那么强大的东西替换,比如monad-prompt。延续是一把大锤,10次中有9次,callCC太强大了,使用更高阶函数和懒惰的组合可以更愉快地表达。

    作为一个例子,callCC的一个原型用法是实现异常之类的东西,但在Haskell中我们可以使用monads :)(它依赖于高阶函数和懒惰)。

    从本质上讲,你的建议增加了复杂性,意味着将MTL合并到基础中,以及一大堆其他不愉快的东西,以避免liftIO

    RE编辑

    1. 当然你可以这样做,但当然它不是MonadCont的一个实例:)
    2. 这有点不同,unsafePerformIO意在使用,以便副作用对任何人都不可见,因为你无法保证如何或何时执行。

      如果callCCIO属于这种情况,那么您可以使用Cont

    3. 延续传递风格很有用,我们拥有ConT r IO!对于我来说,这是棺材中最大的钉子,对于这个想法,我没有看到任何好处而不仅仅是使用现有的库而不是一个困难且可能不安全的编译器黑客。

      < / LI>

答案 1 :(得分:1)

通过使用ContT monad进行简单的中止功能,您实际上可以用火箭筒击中苍蝇。 :-)如果您的目标只是在出现问题时中止返回错误消息,那么您可能需要考虑使用ErrorT monad(由transformers提供)。或者,如果在发生错误时您不会为结果使用单独的类型,那么您可能需要考虑使用AbortT monad(由AbortT-transformers提供)。

答案 2 :(得分:1)

答案是否定的,因为call / cc对任何语言都是一个糟糕的功能。尽管call / cc已经在Scheme中使用了很长时间,但这并不意味着call / cc是一个很好的语言功能。 Scheme中使用call / cc的经验表明它是一个糟糕的语言特性:使用call / cc经常泄漏内存,没有dynamic-wind的call / cc不适合任何严肃的程序或任何库;另一方面,使用动态风,标准呼叫/ cc惯用语变慢。列举了所有这些缺点,其中包含支持代码和其他证据 http://okmij.org/ftp/continuations/against-callcc.html

我同意延续传递风格有很多好处。事实上,当我们以monadic风格编写Haskell代码时,我们正好使用CPS。实际上,请考虑一个简单的嵌套函数调用putChar (getChar ())。我们在CPS中写下如下

 getCharK :: () -> (Char -> w) -> w
 putCharK :: Char -> (() -> w) -> w

 gp :: (() -> w) -> w
 gp = \k -> getCharK () (\c -> putCharK c k)

这里我们如何为monadic getM和putM(对于某些monad M)编写相同的嵌套调用:

getM :: () -> M Char
putM :: Char -> M ()

gpM :: M ()
gpM =  getM () `bind` (\c -> putM c)

现在,如果bind被定义为

bind m f = \k -> m (\x -> (f x) k)

然后monadic嵌套函数应用程序与代码的CPS版本相同。 如果用ContT w IO和newtype替换M,那么monadic示例在Haskell中有效 在bind的定义中包装ContT / runContT(在ContT w IO monad中变为(>>=))。所以,Haskell已经让我们在CPS中编写代码了; do-nonation使它非常方便。

答案 3 :(得分:0)

文档说:

  

许多需要其他语言延续的算法都没有   由于Haskell的懒惰语义,在Haskell中需要它们。虐待   延续monad可以生成无法理解的代码   并保持

我想这可能是主要的原因。