Haskell IO i zamykanie plików

Kiedy otwieram plik do czytania w Haskell, odkryłem, że nie mogę użyć zawartości pliku po zamknięciu go. Na przykład ten program wyświetli zawartość pliku:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          putStr contents
          hClose inFile

Spodziewałem się, że zamiana linii putStr z linią hClose nie przyniesie żadnego efektu, ale ten program nic nie wyświetla:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          hClose inFile
          putStr contents
Dlaczego tak się dzieje? Domyślam się, że ma to coś wspólnego z leniwą oceną, ale myślałem, że te wyrażenia zostaną zsekwencjonowane, więc tam nie byłoby problemu. Jak zaimplementowałbyś funkcję readFile?
Author: Don Stewart, 2008-11-17

6 answers

Jak stwierdzili inni, wynika to z leniwej oceny. Uchwyt jest w połowie zamknięty po tej operacji i zostanie automatycznie zamknięty po odczytaniu wszystkich danych. Zarówno hGetContents, jak i readFile są w ten sposób leniwe. W przypadkach, gdy masz problemy z otwartymi uchwytami, Zwykle wymusza się odczyt. Oto prosty sposób:

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

W dzisiejszych czasach jednak nikt już nie używa ciągów znaków do wejścia/wyjścia plików. Nowym sposobem jest wykorzystanie danych.ByteString (dostępny na hackage), oraz Data.ByteString.Leniwy kiedy chcesz [[6]}leniwy czyta.

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

Bytestringi to sposób na duże łańcuchy znaków (np. zawartość pliku). Są one znacznie szybsze i wydajniejsze niż String (=[Char]).

Uwagi:

Zaimportowałem rnf z Control.Równolegle.Strategie tylko dla wygody. Można by coś takiego napisać samemu dość łatwo:

  forceList [] = ()
  forceList (x:xs) = forceList xs

Wymusza to tylko przesunięcie kręgosłupa (nie wartości) listy, które miałoby efekt odczytu całego pliku.

Leniwe I/o staje się uważane przez ekspertów za złe; zalecam używanie ścisłych bajtów dla większości We/Wy plików na razie. W piecu jest kilka rozwiązań, które próbują przywrócić komponowalne odczyty przyrostowe, z których najbardziej obiecująca jest tzw. "Iteratee" Olega.

 39
Author: luqui,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-08 12:32:34

[Update : Prelude.readFile powoduje problemy opisane poniżej, ale przejście na używanie danych.Wersje ByteString wszystko działa: nie dostaję już wyjątku.]

Haskell newbie tutaj, ale obecnie nie kupuję twierdzenia, że "readFile jest ścisły, i zamyka plik, gdy jest zrobione":

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 ()

To działa tak, jak na pliku, z którym testowałem, ale jeśli skomentujesz putStrLn, to najwyraźniej plik writeFile zawiedzie. (Ciekawe jak lamerskie Komunikaty WYJĄTKÓW Haskell są, brak numerów linii itp.?)

Test> go "Foo.hs"
reading
writing
Exception: Foo.hs: openFile: permission denied (Permission denied)
Test> 

?!?!?

 4
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-19 01:58:49

Dzieje się tak dlatego, że hGetContents jeszcze nic nie robi: jest to leniwe we / wy. tylko gdy użyjesz ciągu wynikowego plik jest rzeczywiście odczytany (lub jego część, która jest potrzebna). Jeśli chcesz wymusić jego odczyt, możesz obliczyć jego długość i użyć funkcji seq, aby wymusić Długość do obliczenia. Leniwe I / O może być fajne, ale może być również mylące.

Aby uzyskać więcej informacji, zobacz Część o leniwych I/O w prawdziwym świecie Haskell, na przykład.

 2
Author: Erik Hesselink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-17 22:58:06

Jak już wspomniano, hGetContents jest leniwy. readFile jest ścisłe i zamyka plik po zakończeniu:

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

Daje następujące w uściskach

> main
blahblahblah

Gdzie foo jest

blahblahblah

Co ciekawe, seq zagwarantuje tylko, że odczytana zostanie część danych wejściowych, a nie całość:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents $! inFile
          contents `seq` hClose inFile
          putStr contents

> main
b

Dobrym zasobem jest: tworzenie programów Haskell szybszych i mniejszych: hGetContents, hClose, readFile

 1
Author: Chris Conway,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-18 02:06:46

Jeśli chcesz, aby Twoje IO było leniwe, ale aby zrobić to bezpiecznie, aby takie błędy nie wystąpiły, użyj pakietu zaprojektowanego do tego celu, takiego jak safe-lazy-io. (Jednak safe-Lazy-io nie obsługuje bajtestring I / O.)

 1
Author: Robin Green,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-03 10:42:17

Wyjaśnienie jest dość długie. Proszę mi wybaczyć tylko krótką końcówkę: należy przeczytać o "półzamkniętych uchwytach do plików" i "unsafePerformIO".

W skrócie-to zachowanie jest konstrukcyjnym kompromisem między jasnością semantyczną a leniwą oceną. Powinieneś albo odłożyć hClose, dopóki nie będziesz absolutnie pewien, że nie będziesz robił nic z zawartością pliku( np. wywołaj go w programie obsługi błędów lub innym), lub użyj czegoś innego niż hGetContents, aby uzyskać zawartość pliku nie-leniwie.

 0
Author: ADEpt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-17 20:52:42