Haskell getContents 等待 EOF

Haskell getContents wait for EOF

我想等到用户输入以 EOF 终止,然后将其全部输出。这不是getContents应该做的吗?每次用户点击回车时都会输出以下代码,我做错了什么?

import System.IO

main = do
  hSetBuffering stdin NoBuffering
  contents <- getContents
  putStrLn contents

根本问题是 getContentsLazy IO 的实例。这意味着 getContents 产生一个 thunk,可以像正常 Haskell 值一样进行评估,并且仅在强制 .

时才执行相关的 IO

contentsputStr 尝试打印的惰性列表,它会强制列表并导致 getContents 尽可能多地读取。 putStr 然后打印强制的所有内容,并继续尝试强制列表的其余部分,直到它达到 []。由于 getContents 可以读取越来越多的流——确切的行为取决于缓冲——putStr 可以立即打印越来越多的流,给你你看到的行为。

虽然此行为对于非常简单的脚本很有用,但它会将 Haskell 的评估顺序与 可观察的 效果联系起来——这是它从未打算做的事情。这意味着精确控制 contents 的某些部分何时打印是很尴尬的,因为你必须打破正常的 Haskell 抽象并准确理解事物是如何被评估的。

这会导致一些潜在的非直觉行为。例如,如果您尝试获取输入的长度 - 实际上 使用 它 - 在您开始打印之前强制列表,为您提供您想要的行为:

main = do
  contents <- getContents
  let n = length contents
  print n
  putStr contents

但是如果您将 print n 移到 putStr 之后,您会回到原来的行为,因为 n 直到 之后 [=60] 才会被强制=] 打印输入(即使在使用 putStr 之前 n 仍然得到 defined

main = do
  contents <- getContents
  let n = length contents
  putStr contents
  print n

通常,这种事情不是问题,因为它不会改变代码的行为(尽管它会影响性能)。 Lazy IO只是通过打通抽象层,将其带入了正确的境界。

这也给了我们如何解决您的问题的提示:我们需要某种方式在打印之前强制 contents。正如我们所看到的,我们可以用 length 来做到这一点,因为 length 需要在计算结果之前遍历整个列表。我们可以使用 seq 代替打印它,这会强制左侧表达式与右侧表达式同时求值,但会丢弃实际值:

main = do
  contents <- getContents
  let n = length contents
  n `seq` putStr contents

同时,这仍然有点难看,因为我们使用 length 只是为了遍历列表,而不是因为我们真的关心它。我们真正想要的是 just 遍历列表足以评估它的函数,而不做任何其他事情。令人高兴的是,这正是 deepseq 所做的(对于许多数据结构,而不仅仅是列表):

import Control.DeepSeq
import System.IO

main = do
  contents <- getContents
  contents `deepseq` putStr contents

这是懒惰的问题I/O。一种简单的解决方案是使用严格的 I/O,例如通过 ByteStrings:

import qualified Data.ByteString as S

main :: IO ()
main = S.getContents >>= S.putStr

您可以使用 strict 包中的替换函数 (link):

import qualified System.IO.Strict as S

main = do
  contents <- S.getContents
  putStrLn contents

请注意,读取不需要设置缓冲。缓冲实际上只在写入文件时有用。有关详细信息,请参阅此答案

System.IO.Strict中hGetContents的严格版本的定义很简单:

hGetContents    :: IO.Handle -> IO.IO String
hGetContents h  = IO.hGetContents h >>= \s -> length s `seq` return s

即,它通过对 hGetContents 的 standard/lazy 版本返回的字符串调用 length 来强制将所有内容读入内存。