hGetContents如何实现内存效率?

时间:2013-10-17 07:15:00

标签: lazy-evaluation haskell lazy-io

我想将Haskell添加到我的工具箱中,所以我正在通过Real World Haskell

在输入和输出一章的the section on hGetContents中,我看到了这个例子:

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
    inh <- openFile "input.txt" ReadMode
    outh <- openFile "output.txt" WriteMode
    inpStr <- hGetContents inh
    let result = processData inpStr
    hPutStr outh result
    hClose inh
    hClose outh

processData :: String -> String
processData = map toUpper

根据此代码示例,作者接着说:

  

请注意hGetContents为我们处理了所有阅读。另外,请查看processData。它是一个纯函数,因为它没有副作用,并且每次调用时总是返回相同的结果。在这种情况下,它无需知道 - 并且无法告诉 - 它的输入是从文件中懒惰地读取的。 它可以很好地与磁盘上的20个字符的文字或500GB的数据转储一起使用。 (N.B。强调是我的)

我的问题是:hGetContents或其结果值如何实现此内存效率 - 在此示例中 - processData“能够告诉”,并仍然保留纯代码产生的所有好处(即processData),特别是memoization?

<- hGetContents inh返回一个字符串,因此inpStr绑定到String类型的值,这正是processData接受的类型。但是,如果我正确理解了真实世界Haskell的作者,那么这个字符串就不像其他字符串那样,因为它没有完全加载到内存中(或者如果存在未完全评估的字符串这样的东西,则完全评估它。 。)到调用processData时。

因此,问我问题的另一种方法是:如果inpStr在调用processData时没有完全评估或加载到内存中,那么它如何用于查找是否对processData的记忆调用存在,而没有先完全评估inpStr

是否存在String类型的实例,每个实例的行为都不同但在此抽象级别上无法分开?

1 个答案:

答案 0 :(得分:4)

String返回的hGetContents与任何其他Haskell字符串没有区别。一般情况下,除非程序员采取额外措施来确保它(例如seqdeepseq,爆炸模式),否则不会对Haskell数据进行全面评估。

字符串定义为(基本上)

data List a = Nil | Cons a (List a) -- Nil === [], Cons === :
type String = List Char

这意味着字符串是空的,或者是单个字符(头部)和另一个字符串(尾部)。由于laziness,尾巴可能不存在于记忆中,甚至可能是无限的。在处理String时,Haskell程序通常会检查它是Nil还是Cons,然后根据需要进行分支和继续。如果函数不需要评估尾部,则不会。例如,此函数只需要检查初始构造函数:

safeHead :: String -> Maybe Char
safeHead [] = Nothing
safeHead (x:_) = Just x

这是一个完全合法的字符串

allA's = repeat 'a' :: String

这是无限的。您可以合理地操作此字符串,但是如果您尝试打印所有字符串,或计算长度,或任何类型的无界遍历,您的程序将不会终止。但是你可以使用像safeHead这样的函数而不会有任何问题,甚至可以使用一些有限的初始子字符串。

然而,你发现某些奇怪事情的直觉是正确的。 hGetContents是使用特殊函数unsafeInterleaveIO实现的,它本质上是IO行为的编译器钩子。对此越少说越好。

你是正确的,如果没有完全评估参数,就很难检查是否存在对函数的memoized调用。但是,大多数编译器不执行此优化。问题是编译器很难确定何时值得记住调用,并且很容易通过这样做来消耗所有内存。幸运的是,您可以使用several memoizing libraries在适当的时候添加备忘录。