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





Объяснение здесь довольно длинное. Простите меня за то, что дал только небольшой совет: вам нужно прочитать о «полузакрытых дескрипторах файлов» и «unsafePerformIO».
Короче говоря, такое поведение представляет собой компромисс между семантической ясностью и ленивой оценкой. Вам следует либо отложить hClose до тех пор, пока вы не будете абсолютно уверены, что не будете делать что-либо с содержимым файла (например, вызвать его в обработчике ошибок или что-то подобное), либо использовать что-то еще, кроме hGetContents, чтобы получить содержимое файла не лениво.
Я не думаю, что unsafePerformIO здесь уместен. Может быть, unsafeInterleaveIO.
Это потому, что hGetContents еще ничего не делает: это ленивый ввод-вывод. Только когда вы используете результирующую строку, файл фактически считывается (или его часть, которая необходима). Если вы хотите принудительно прочитать его, вы можете вычислить его длину и использовать функцию seq для принудительного вычисления длины. Ленивый ввод-вывод может быть крутым, но также может сбивать с толку.
Для получения дополнительной информации, например, см. часть о ленивом вводе / выводе в Real World Haskell.
Как отмечалось ранее, 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
readFile использует hGetContents и не закрывает файл. Это лениво, если верить Real World Haskell и самому исходному коду.
Во-первых, readFile не является строгим, как уже упоминалось, во-вторых, использование $! с hGetContents полностью избыточно.
Как утверждали другие, это из-за ленивой оценки. После этой операции ручка будет полузакрыта и закроется автоматически, когда будут прочитаны все данные. И 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".
Два комментария. Во-первых, многие люди до сих пор используют строки для файлового ввода-вывода. Они прекрасны, когда вы хотите получить из файла строку! Во-вторых, многие люди не считают ленивый ввод-вывод злом, но он считается хитрым. Это позволяет нам делать всевозможные изящные вещи с очень низкими синтаксическими издержками, но за счет сохранения определенных ограниченных типов операционных рассуждений наряду с уравнениями.
Наткнулся на этот ответ и спасибо, @liqui! Просто хотел указать (3 года спустя), что ваш rnf должен быть: rnf contents 'seq' hClose inFile, с обратными кавычками вокруг seq. Кроме того, rnf был перемещен в Control.DeepSeq.
@Peter, я думаю, мы говорили о ленивый IO, который в вашем комментарии не рассматривается.
«Ленивый ввод-вывод в серьезном серверном программировании непрофессионален» - Олег Киселев
[Обновлять: 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>
?!?!?
Я только что запустил твой код. GHCi говорит: openFile: resource busy (file is locked). Это соответствовало бы ленивости readFile.
Если вы хотите, чтобы ваш ввод-вывод был ленивым, но чтобы сделать это безопасно, чтобы не возникало подобных ошибок, используйте пакет, предназначенный для этого, например безопасный ленивый. (Однако safe-lazy-io не поддерживает ввод-вывод байтов.)
Не могли бы вы дать ссылки на какие-нибудь полезные статьи по этим темам? Мне не удалось найти ничего, кроме скудной документации и сообщений из списков рассылки по конкретным вопросам.