我正在读《为你学习一个伟大的好事的哈斯克尔》第9章:输入和输出。有一个用于解释流的示例代码:
main = do
withFile "something.txt" ReadMode (handle -> do
contents <- hGetContents handle
putStr contents)
书上说:
这就是为什么在这种情况下它实际上读取一行,将其打印到 输出,读取下一行,打印它,等等。
但是在之前的内容中,对于同一示例,它还说:
这真的很酷,因为我们可以将内容视为整个内容。 的文件,但它并没有真正加载到内存中。
我是函数式编程的新手,我对此感到非常困惑,如果一次读取一行,为什么我们可以contents
作为全部内容?我虽然contents <- hGetContents handle
中的contents
只是一行的内容,Haskell是将每一行的内容保存到临时内存中还是其他什么?
如果内容一次读取一行,为什么我们可以将内容视为整个内容?
首先请注意,没有必要逐行阅读内容(尽管有可能,但我稍后会谈到这一点)。作者的意思是,即使整个文件没有加载到内存中,您也可以从概念上假设变量contents
具有文件的全部内容。这是可能的,因为文件的延迟流式传输(如果您更感兴趣,您可以查看源以查看低级详细信息。它基本上使用unsafeInterleaveIO
来实现这一点)。
Haskell是否将每一行的内容保存到临时内存或其他内容中?
这取决于所使用的缓冲类型。根据文档,它取决于底层文件系统:
打开句柄时的默认缓冲模式为 依赖于实现,可能依赖于文件系统对象 连接到该手柄。对于大多数实现,物理 文件通常是块缓冲的,终端通常是 行缓冲。
但是您可以使用hGetBuffering :: Handle -> IO BufferMode
来查看自己所处的缓冲模式。
您可以将其视为一个函数,该函数如何理解哈斯克尔中的流
在调用时返回一些结果(不是全部)以及一个回调函数,以在需要时获取其余结果。因此,从技术上讲,它为您提供了整个内容,但一次只能提供一块,并且只有在您要求其余内容时才提供。
如果Haskell没有非严格的语义,你可以通过这样的东西来实现这个概念:
data Stream a = Stream [a] (() -> Stream a)
instance (Show a) => Show (Stream a) where
show (Stream xs _) = show xs ++ " ..."
rest :: Stream a -> Stream a -- ask for the rest of the stream
rest (Stream _ f) = f ()
然后假设你想要一个迭代整数的流。您可以返回前 3 个并推迟其余部分,直到用户要求它:
iter :: Int -> Stream Int
iter x = Stream [x, x + 1, x + 2] (_ -> iter (x + 3))
然后
> iter 0
[0,1,2] ...
但是如果你继续要求其余的,你会得到整个内容
> take 5 $ iterate rest (iter 0)
[[0,1,2] ...,[3,4,5] ...,[6,7,8] ...,[9,10,11] ...,[12,13,14] ...]
或
> let go (Stream [i, j, k] _) acc = i:j:k:acc
> take 20 . foldr go [] $ iterate rest (iter 0)
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
引擎盖下的线路缓冲也是如此。它读取并返回第一行,但随后您可以请求下一行和下一行,...因此,从技术上讲,即使一次只读取一行,您也可以获得整个内容。
请注意,hGetHandle
本身从不读取任何内容;它只是返回一个IO
操作,当最终执行时,将从文件中读取。当与putStr
with hGetContents handle >>= putStr
(只是脱糖的do
表示法)结合使用时,您将获得一个 IO 操作,该操作采用句柄并将其内容输出到屏幕。在 Haskell 程序本身的任何时候,您都不会指定这是如何发生的;这完全取决于Haskell运行时以及它如何执行您创建IO
操作。