在Haskell中"真正"懒惰的IO

Anu*_*ain 6 monads haskell

考虑片段 -

getLine >>= \_ -> getLine >>= putStr
Run Code Online (Sandbox Code Playgroud)

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

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

runM $ lift getLine >>= \_ -> lift getLine >>= lift putStr
Run Code Online (Sandbox Code Playgroud)

哪里

runM :: M a -> IO a
lift :: IO a -> M a
Run Code Online (Sandbox Code Playgroud)

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

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

ham*_*mar 11

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

首先,lift putStr不会键入check,因为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
Run Code Online (Sandbox Code Playgroud)

然而,正如CA McCann指出的那样,你可能不应该将它用于任何严肃的事情.Lazy IO已经不赞成了,因为它很难推断出副作用的实际顺序.这会让它变得更难.

考虑这个例子

main = runM $ do
    foo <- lazy readLn
    bar <- lazy readLn
    return $ foo / bar
Run Code Online (Sandbox Code Playgroud)

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

  • 这个monad是邪恶的化身,将填补我的噩梦. (11认同)
  • 像"readFile"这样的东西意义上的"懒惰IO"在特定的(有些常见的)场景中表现得非常好,并且如果使用得当,并不普遍不赞成.我[并不是这个概念的粉丝](http://stackoverflow.com/questions/6668716/haskell-lazy-bytestring-read-write-progress-function/6669453#6669453),但意见不同.另一方面,在任意地方抛弃`unsafeInterleaveIO`确实非常可疑. (5认同)

C. *_*ann 8

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

也就是说,以受控的方式引入这种东西就是"懒惰的IO"已经做到的.对此的"原始"操作,unsafeInterleaveIO大致实现return . unsafePerformIO,加上一些细节,使事情表现得更好.应用unsafeInterleaveIO的一切,通过隐藏在你的"懒IO"单子的绑定操作,可能会完成不明智的概念,你是后.


小智 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) }
Run Code Online (Sandbox Code Playgroud)