Haskell IO и закрытие файлов - PullRequest
38 голосов
/ 17 ноября 2008

Когда я открываю файл для чтения в 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?

Ответы [ 6 ]

37 голосов
/ 18 ноября 2008

Как уже говорили другие, это из-за ленивой оценки. Дескриптор после этой операции наполовину закрыт и автоматически будет закрыт, когда все данные будут прочитаны. И 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

Это просто вызывает обход позвоночника (не значений) списка, что может привести к чтению всего файла.

Эксперты считают ленивый ввод / вывод злым; Я рекомендую использовать строгие байтовые строки для большинства файловых операций ввода-вывода. В духовке есть несколько решений, которые пытаются вернуть составные инкрементные чтения, наиболее многообещающее из которых Олег назвал "Итерирующим".

4 голосов
/ 19 ноября 2008

[ Обновление : Prelude.readFile вызывает проблемы, как описано ниже, но переключение на использование версий Data.ByteString всего работает: я больше не получаю исключение.]

Новичок Хаскелла здесь, но в настоящее время я не покупаю утверждение, что "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> 

?!?!?

2 голосов
/ 18 ноября 2008

Это потому, что hGetContents пока ничего не делает: это ленивый ввод / вывод. Только когда вы используете результирующую строку, файл фактически читается (или его часть). Если вы хотите, чтобы он был прочитан, вы можете вычислить его длину и использовать функцию seq для принудительной оценки длины. Ленивый ввод / вывод может быть крутым, но это также может сбивать с толку.

Для получения дополнительной информации см. часть о ленивом вводе / выводе в реальном мире, например, на Haskell.

1 голос
/ 03 ноября 2010

Если вы хотите сохранить свой IO ленивым, но сделать это безопасно, чтобы такие ошибки не возникали, используйте пакет, разработанный для этого, например safe-lazy-io . (Однако safe-lazy-io не поддерживает ввод-вывод с использованием строк.)

1 голос
/ 18 ноября 2008

Как уже отмечалось, 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

0 голосов
/ 17 ноября 2008

Объяснение довольно длинное, чтобы быть включенным сюда. Извините, что выдаю только короткий совет: вам нужно прочитать о "полузакрытых файловых дескрипторах" и "unsafePerformIO".

Короче говоря - это поведение является компромиссом между семантической ясностью и ленивой оценкой. Вам следует либо отложить hClose до тех пор, пока вы не будете абсолютно уверены, что не хотите что-либо делать с содержимым файла (например, вызывать его в обработчике ошибок или что-то в этом роде), либо использовать что-то еще, кроме hGetContents, для получения содержимого файла без лишних усилий.

...