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?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
40
0
11 843
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

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

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

Не могли бы вы дать ссылки на какие-нибудь полезные статьи по этим темам? Мне не удалось найти ничего, кроме скудной документации и сообщений из списков рассылки по конкретным вопросам.

Jay Conrod 18.11.2008 00:16

Я не думаю, что unsafePerformIO здесь уместен. Может быть, unsafeInterleaveIO.

Ben Millwood 11.06.2012 15:01

Это потому, что 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 и самому исходному коду.

alternative 25.06.2011 18:00

Во-первых, readFile не является строгим, как уже упоминалось, во-вторых, использование $! с hGetContents полностью избыточно.

Ben Millwood 11.06.2012 15:00
Ответ принят как подходящий

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

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

sclv 03.11.2010 17:44

Наткнулся на этот ответ и спасибо, @liqui! Просто хотел указать (3 года спустя), что ваш rnf должен быть: rnf contents 'seq' hClose inFile, с обратными кавычками вокруг seq. Кроме того, rnf был перемещен в Control.DeepSeq.

Xavier Ho 08.06.2011 15:10

@Peter, я думаю, мы говорили о ленивый IO, который в вашем комментарии не рассматривается.

luqui 18.09.2011 20:34

«Ленивый ввод-вывод в серьезном серверном программировании непрофессионален» - Олег Киселев

Mauricio Scheffer 12.03.2012 18:36

[Обновлять: 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.

Jørgen Fogh 06.08.2009 16:13

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

Другие вопросы по теме