我想将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
类型的实例,每个实例的行为都不同但在此抽象级别上无法分开?
答案 0 :(得分:4)
String
返回的hGetContents
与任何其他Haskell字符串没有区别。一般情况下,除非程序员采取额外措施来确保它(例如seq
,deepseq
,爆炸模式),否则不会对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在适当的时候添加备忘录。