3

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

在输入和输出一章中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。它是一个纯函数,因为它没有副作用,并且每次调用时总是返回相同的结果。在这种情况下,它不需要知道——<em>也没有办法知道——它的输入是从文件中懒惰地读取的。它可以与 20 个字符的文字或磁盘上的 500GB 数据转储完美配合。 (NB重点是我的)

我的问题是:如果没有——在这个例子中—— “能够分辨”,hGetContents或者它的结果值如何实现这种内存效率,并且仍然保持纯代码(即)产生的所有好处,特别是记忆化?processDataprocessData

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

因此,问我的问题的另一种方式是:如果inpStr在调用时没有完全评估或加载到内存中,那么如何在没有首先完全评估的情况下processData使用它来查找是否存在记忆调用?processDatainpStr

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

4

1 回答 1

4

String返回的 by与hGetContents任何其他 Haskell 字符串没有什么不同。一般来说,Haskell 数据不会被完全评估,除非程序员已经采取额外的步骤来确保它是(例如seq,,deepseq爆炸模式)。

字符串被定义为(本质上)

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

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

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

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

allA's = repeat 'a' :: String

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

但是,您对正在发生奇怪事情的直觉是正确的。 hGetContents是使用特殊函数unsafeInterleaveIO实现的,它本质上是一个编译器挂钩IO行为。这件事越少说越好。

你是对的,如果没有完全评估参数,就很难检查是否存在对函数的记忆调用。但是,大多数编译器不执行此优化。问题在于,编译器很难确定何时值得记忆调用,而且这样做很容易消耗所有内存。幸运的是,有几个 memoizing 库可以用来在适当的时候添加 memoization。

于 2013-10-17T07:41:27.930 回答