考虑片段 -
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以达到我想要的效果。我不确定它是否可能。有人可以帮忙吗?
答案 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) }