Haskell IO и файлы закрытия
Когда я открываю файл для чтения в Haskell, я обнаружил, что после его закрытия я не могу использовать содержимое файла. Например, эта программа будет печатать содержимое файла:
main = do inFile <- openFile "foo" ReadMode
contents <- hGetContents inFile
putStr contents
hClose inFile
Я ожидал, что переключение строки putStr
с линией hClose
не будет иметь никакого эффекта, но эта программа ничего не печатает:
main = do inFile <- openFile "foo" ReadMode
contents <- hGetContents inFile
hClose inFile
putStr contents
Почему это происходит? Я предполагаю, что это имеет какое-то отношение к ленивой оценке, но я думал, что эти выражения будут упорядочены, поэтому проблем не будет. Как бы вы реализовали функцию типа readFile
?
Ответы
Ответ 1
Как заявили другие, это из-за ленивой оценки. После этой операции рукоятка будет полузамкнута и будет автоматически закрыта при чтении всех данных. Таким образом, hGetContents и readFile являются ленивыми. В случаях, когда у вас проблемы с открытыми ручками, обычно вы просто принудительно читаете. Вот простой способ:
import Control.Parallel.Strategies (rnf)
-- rnf means "reduce to normal form"
main = do inFile <- openFile "foo"
contents <- hGetContents inFile
rnf contents `seq` hClose inFile -- force the whole file to be read, then close
putStr contents
В эти дни, однако, никто больше не использует строки для ввода-вывода файлов. Новый способ - использовать Data.ByteString(доступно для взлома) и Data.ByteString.Lazy, когда вы хотите ленивых чтений.
import qualified Data.ByteString as Str
main = do contents <- Str.readFile "foo"
-- readFile is strict, so the the entire string is read here
Str.putStr contents
ByteStrings - это путь для больших строк (например, содержимого файла). Они намного быстрее и эффективнее памяти, чем String (= [ Char]).
Примечания:
Я импортировал rnf из Control.Parallel.Strategies только для удобства. Вы могли бы написать что-то вроде этого довольно легко:
forceList [] = ()
forceList (x:xs) = forceList xs
Это просто приводит к обходу спинного хребта (а не значений) списка, что может повлиять на чтение всего файла.
Ленивый ввод-вывод становится экспертом злым; Я рекомендую использовать строгие байты для большинства операций ввода-вывода файлов. В духовке есть несколько решений, которые пытаются вернуть композиционные инкрементные чтения, наиболее перспективным из которых Олегом называется "Итератез".
Ответ 2
[ Обновление: Prelude.readFile вызывает проблемы, описанные ниже, но переключение на использование версий всех версий .B.BitteString: я больше не получаю исключения.]
Haskell новичок здесь, но в настоящее время я не покупаю требование, что "readFile является строгим и закрывает файл, когда он был сделан":
go fname = do
putStrLn "reading"
body <- readFile fname
let body' = "foo" ++ body ++ "bar"
putStrLn body' -- comment this out to get a runtime exception.
putStrLn "writing"
writeFile fname body'
return ()
Это работает так же, как и в файле, который я тестировал, но если вы закомментируете putStrLn, то, по-видимому, сбой writeFile. (Интересно, как хромают сообщения об исключениях Haskell, не имеют номеров строк и т.д.?)
Test> go "Foo.hs"
reading
writing
Exception: Foo.hs: openFile: permission denied (Permission denied)
Test>
?!?!?
Ответ 3
Это потому, что hGetContents ничего не делает: это ленивый ввод-вывод. Только когда вы используете результирующую строку, файл фактически читается (или его часть, которая необходима). Если вы хотите заставить его читать, вы можете вычислить его длину и использовать функцию seq, чтобы заставить длину оценивать. Lazy I/O может быть классным, но он также может быть запутанным.
Для получения дополнительной информации см. часть о ленивом вводе/выводе в Real World Haskell, например.
Ответ 4
Как отмечалось ранее, hGetContents
ленив. readFile
является строгим и закрывает файл, когда это делается:
main = do contents <- readFile "foo"
putStr contents
дает в объятиях
следующее:
> main
blahblahblah
где foo
-
blahblahblah
Интересно, что seq
гарантирует только чтение некоторой части ввода, а не всего:
main = do inFile <- openFile "foo" ReadMode
contents <- hGetContents $! inFile
contents `seq` hClose inFile
putStr contents
дает
> main
b
Хороший ресурс: Создание программ Haskell быстрее и меньше: hGetContents, hClose, readFile
Ответ 5
Если вы хотите, чтобы ваш IO ленивый, но чтобы сделать это безопасно, чтобы такие ошибки не возникали, используйте пакет, предназначенный для этого, например safe-lazy-io. (Тем не менее, safe-lazy-io не поддерживает байтов ввода-вывода.)
Ответ 6
Объяснение довольно длинное, чтобы быть включенным здесь. Простите меня за то, что вы раздавали только короткий совет: вам нужно прочитать о "полузакрытых ручках файлов" и "unsafePerformIO".
Вкратце - это поведение является компрометацией дизайна между смысловой ясностью и ленивой оценкой. Вы должны либо отложить hclose, пока не убедитесь, что вы ничего не будете делать с содержимым файла (например, вызовите его в обработчике ошибок или somesuch), или используйте что-то еще, кроме hGetContents, чтобы получить содержимое файла не лениво.