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?


person Jay Conrod    schedule 17.11.2008    source источник


Ответы (6)


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

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

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

person luqui    schedule 18.11.2008
comment
Два комментария. Во-первых, многие до сих пор используют строки для файлового ввода-вывода. Они прекрасно подходят, когда вы хотите получить из файла строку! Во-вторых, Lazy IO многие не считают злом, но считают его хитрым. Это позволяет нам делать всевозможные изящные вещи с очень низкими синтаксическими затратами, но ценой поддержки некоторых ограниченных типов операционных рассуждений наряду с эквациональными рассуждениями. - person sclv; 03.11.2010
comment
Наткнулся на этот ответ и спасибо, @liqui! Просто хотел указать (3 года спустя), что ваш rnf должен быть: rnf contents 'seq' hClose inFile, с обратными кавычками вокруг seq. Кроме того, rnf был перемещен в Control.DeepSeq. - person Xavier Ho; 08.06.2011
comment
@Peter, я думаю, мы говорили о ленивом вводе-выводе, который не касается вашего комментария. - person luqui; 18.09.2011
comment
Ленивый ввод-вывод в серьезном программировании на стороне сервера — это непрофессионально — Олег Киселев - person Mauricio Scheffer; 12.03.2012

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

Новичок в 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> 

?!?!?

person Community    schedule 19.11.2008
comment
Я только что запустил ваш код. GHCI говорит: openFile: resource busy (file is locked). Это согласуется с ленивым readFile. - person Jørgen Fogh; 06.08.2009

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

Для получения дополнительной информации см. часть об отложенном вводе-выводе в Real Например, World Haskell.

person Erik Hesselink    schedule 17.11.2008

Как отмечалось ранее, hGetContents ленив. readFile является строгим и закрывает файл, когда это делается:

main = do contents <- readFile "foo"
          putStr contents

дает следующее в Hugs

> 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< /а>

person Chris Conway    schedule 17.11.2008
comment
readFile использует hGetContents и не закрывает файл. Это лениво, согласно Real World Haskell и самому исходному коду. - person alternative; 25.06.2011
comment
Во-первых, readFile не является строгим, как упоминалось, во-вторых, использование $! с hGetContents совершенно избыточно. - person Ben Millwood; 11.06.2012

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

person Robin Green    schedule 03.11.2010

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

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

person ADEpt    schedule 17.11.2008
comment
Не могли бы вы дать ссылку на что-нибудь хорошее, что можно прочитать по этим темам? Мне не удалось найти ничего, кроме скудной документации и сообщений из списка рассылки по конкретным вопросам. - person Jay Conrod; 18.11.2008
comment
Я не думаю, что unsafePerformIO здесь уместно. Возможно unsafeInterleaveIO. - person Ben Millwood; 11.06.2012