Как уже говорили другие, это из-за ленивой оценки. Дескриптор после этой операции наполовину закрыт и автоматически будет закрыт, когда все данные будут прочитаны. И 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
Это просто вызывает обход позвоночника (не значений) списка, что может привести к чтению всего файла.
Эксперты считают ленивый ввод / вывод злым; Я рекомендую использовать строгие байтовые строки для большинства файловых операций ввода-вывода. В духовке есть несколько решений, которые пытаются вернуть составные инкрементные чтения, наиболее многообещающее из которых Олег назвал "Итерирующим".