“确实” Haskell 中的惰性 IO

发布于 2024-12-01 21:26:35 字数 543 浏览 1 评论 0原文

考虑片段 -

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

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

我需要的是将 IO Monad 包装到另一个 Monad (M) 中,该 Monad 允许 IO 计算有效地执行 NOP,除非使用它们的返回值。这样上面的程序就可以重写为类似 -Where

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

并且

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

用户只被要求输入一次。

但是,我不知道如何编写这个 Monad 来达到我想要的效果。我不确定这是否可能。有人可以帮忙吗?

Consider the fragment -

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

It does the reasonable thing, asking for a string twice, and then printing the last input. Because the compiler has no way of knowing what outside effects getLine has, it has to execute both of them, even though we throw away the result of the first one.

What I need is to wrap the IO Monad into another Monad (M) that allows IO computations to be effectively NOPs unless their return values are used. So that the program above could be rewritten as something like -

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

Where

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

And the user is asked for input only once.

However, I cannot figure out how to write this Monad to achieve the effect I want. I'm not sure if it's even possible. Could someone please help?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

情场扛把子 2024-12-08 21:26:35

惰性 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

然而,正如 CA McCann 指出的,你可能不应该使用这个对于任何严重的事情。惰性 IO 已经不受欢迎了,因为它使得很难推断副作用的实际顺序。这会让事情变得更加困难。

考虑这个例子

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

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

Lazy IO is usually implemented using unsafeInterleaveIO :: IO a -> IO a, which delays the side effects of an IO action until its result is demanded, so we'll probably have to use that, but let's get some minor problems out of the way first.

First of all, lift putStr would not type check, as putStr has type String -> IO (), and lift has type IO a -> M a. We'll have to use something like lift . putStr instead.

Secondly, we're going to have to differentiate between IO actions that should be lazy and those who should not. Otherwise the putStr will never be executed, as we're not using it's return value () anywhere.

Taking that into account, this seems to work for your simple example, at least.

{-# 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

However, as C. A. McCann points out, you should probably not use this for anything serious. Lazy IO is frowned upon already, as it makes it difficult to reason about the actual order of the side effects. This would make it even harder.

Consider this example

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

The order of the two numbers are read in will be completely undefined, and may change depending on compiler version, optimizations or the alignment of the stars. The name unsafeInterleaveIO is long and ugly for a good reason: to remind you of the dangers of using it. It's a good idea to let people know when it's being used and not hide it in a monad.

戏蝶舞 2024-12-08 21:26:35

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

也就是说,以受控方式引入此类事物是“Lazy IO”已经做的事情。其“原始”操作是 unsafeInterleaveIO,它大致实现为 return 。 unsafePerformIO,加上一些细节以使事情表现得更好一些。将 unsafeInterleaveIO 应用于所有内容,通过将其隐藏在“惰性 IO”单子的绑定操作中,可能会实现您所追求的不明智的想法。

There's no sensible way to do this, because to be quite honest it's not really a sensible thing to do. The entire purpose for introducing monadic I/O was to give a well-defined ordering to effects in the presence of lazy evaluation. It is certainly possible to throw that out the window if you really must, but I'm not sure what actual problem this would solve other than making it easier to write confusingly buggy code.

That said, introducing this sort of thing in a controlled fashion is what "Lazy IO" already does. The "primitive" operation for that is unsafeInterleaveIO, which is implemented roughly as return . unsafePerformIO, plus some details to make things behave a bit nicer. Applying unsafeInterleaveIO to everything, by hiding it in the bind operation of your "lazy IO" monad, would probably accomplish the ill-advised notion you're after.

你穿错了嫁妆 2024-12-08 21:26:35

您正在寻找的并不是真正的单子,除非您想使用不安全的东西,例如 unsafeInterleaveIO

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

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

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

What you are looking for isn't really a monad, unless you want to work with unsafe stuff like unsafeInterleaveIO.

Instead, a much cleaner abstraction here is Arrow.
I think, the following could work:

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

newtype Lazy m a b =
    Lazy { getLazy :: Promise m a -> m (Promise m b) }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文