在Haskell中“真正”懒惰的IO

时间:2011-08-25 13:19:22

标签: haskell monads

考虑片段 -

getLine >>= \_ -> getLine >>= putStr

它是合理的,要求两次字符串,然后打印最后一个输入。因为编译器无法知道外部效应getLine有什么,所以它必须执行它们,即使我们丢弃了第一个的结果。

我需要的是将IO Monad包装到另一个Monad(M)中,允许IO计算有效地为NOP,除非使用它们的返回值。所以上面的程序可以改写为 -

runM $ lift getLine >>= \_ -> lift getLine >>= lift putStr

其中

runM :: M a -> IO a
lift :: IO a -> M a

并且仅要求用户输入一次。

然而,我无法弄清楚如何编写这个Monad以达到我想要的效果。我不确定它是否可能。有人可以帮忙吗?

3 个答案:

答案 0 :(得分:11)

懒惰IO通常使用unsafeInterleaveIO :: IO a -> IO a来实现,这会延迟IO操作的副作用,直到需要它的结果,所以我们可能不得不使用它,但是让我们解决一些小问题第一

首先,lift putStr不会输入检查,因为putStr的类型为String -> IO (),而lift的类型为IO a -> M a。我们必须使用类似lift . putStr的内容。

其次,我们必须区分应该是懒惰的IO操作和不应该操作的IO操作。否则,putStr永远不会被执行,因为我们在任何地方都没有使用它的返回值()

考虑到这一点,这似乎适用于您的简单示例,至少。

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import System.IO.Unsafe

newtype M a = M { runM :: IO a }
    deriving (Monad)

lazy :: IO a -> M a
lazy = M . unsafeInterleaveIO

lift :: IO a -> M a
lift = M

main = runM $ lazy getLine >> lazy getLine >>= lift . putStr

然而,作为C. A. McCann points out,您可能不应该将此用于任何严重的事情。 Lazy IO已经不赞成了,因为它很难推断出副作用的实际顺序。这会让它变得更难。

考虑这个例子

main = runM $ do
    foo <- lazy readLn
    bar <- lazy readLn
    return $ foo / bar

读入的两个数字的顺序将完全未定义,并且可能会根据编译器版本,优化或星形对齐而改变。 unsafeInterleaveIO这个名字长而丑陋是有充分理由的:提醒你使用它的危险。让人们知道它何时被使用而不是隐藏在monad中是一个好主意。

答案 1 :(得分:8)

没有明智的方法可以做到这一点,因为说实话,这并不是一件明智的事情。引入monadic I / O的整个目的是在存在惰性求值的情况下为效果提供明确定义的排序。如果你真的必须把它扔出窗外当然是可能的,但是我不确定这会解决什么实际问题,除了让你更容易编写令人困惑的错误代码。

也就是说,以受控的方式引入这种东西就是“懒惰IO”已经做到的。对此的“原始”操作是unsafeInterleaveIO,其大致实现为return . unsafePerformIO,加上一些细节使事情表现得更好。将unsafeInterleaveIO应用于所有内容,通过将其隐藏在“懒惰IO”monad的绑定操作中,可能会完成您所追求的不明智的概念。

答案 2 :(得分:5)

除非你想使用像unsafeInterleaveIO这样不安全的东西,否则你所寻找的并不是真正的单子。

相反,这里更清晰的抽象是箭头 我认为,以下方法可行:

data Promise m a
    = Done a
    | Thunk (m a)

newtype Lazy m a b =
    Lazy { getLazy :: Promise m a -> m (Promise m b) }