withFile против openFile
Эта программа производит вывод, который я ожидаю, когда задан входной файл текста, разделенный \n:
import System.IO
main :: IO ()
main = do h <- openFile "test.txt" ReadMode
xs <- getlines h
sequence_ $ map putStrLn xs
getlines :: Handle -> IO [String]
getlines h = hGetContents h >>= return . lines
Заменив withFile на openFile и немного переставив
import System.IO
main :: IO ()
main = do xs <- withFile "test.txt" ReadMode getlines
sequence_ $ map putStrLn xs
getlines :: Handle -> IO [String]
getlines h = hGetContents h >>= return . lines
У меня вообще нет выхода. Я в тупике.
Изменить: больше не в тупик: спасибо всем за вдумчивые и наводящие на размышления ответы. Я немного больше прочитал в документации и узнал, что withFile можно понимать как частичное применение скобок.
Вот чем я закончил:
import System.IO
main :: IO ()
main = withFile "test.txt" ReadMode $ \h -> getlines h >>= mapM_ putStrLn
getlines :: Handle -> IO [String]
getlines h = lines `fmap` hGetContents h
5 ответов
Файл закрывается слишком рано. Из документации:
Дескриптор будет закрыт при выходе из withFile
Это означает, что файл будет закрыт, как только withFile
функция возвращает.
Так как hGetContents
и друзья ленивы, он не будет пытаться прочитать файл, пока не будет принудительно с putStrLn
но к тому времени withFile
закрыл бы файл уже.
Чтобы решить проблему, передайте все это withFile
:
main = withFile "test.txt" ReadMode $ \handle -> do
xs <- getlines handle
sequence_ $ map putStrLn xs
Это работает, потому что к тому времени withFile
доходит до закрытия файла, вы бы уже распечатали его.
Тьфу, никто никогда не давал простое решение?
main :: IO ()
main = do xs <- fmap lines $ readFile "test.txt"
mapM_ putStrLn xs
Не использовать openFile
+hGetContents
или же withFile
+hGetContents
когда вы можете просто использовать readFile
, С readFile
Вы не можете выстрелить себе в ногу, закрыв файл слишком рано.
Они делают совершенно разные вещи.openFile
открывает файл и возвращает дескриптор файла:
openFile :: FilePath -> IOMode -> IO Handle
withFile
используется, чтобы обернуть вычисление ввода-вывода, которое берет дескриптор файла, гарантируя, что впоследствии дескриптор будет закрыт:
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
В вашем случае использование withFile будет выглядеть так:
main = withFile "test.txt" ReadMode $ \h -> do
xs <- getlines h
sequence_ $ map putStrLn xs
Версия, которая у вас есть, откроет файл, позвоните getlines
, затем закройте файл. поскольку getlines
ленив, он не будет читать какие-либо выходные данные, пока файл открыт, и как только файл будет закрыт, он не сможет.
Вы сталкиваетесь с обычными препятствиями ленивого ввода-вывода... ленивый ввод-вывод звучит как отличная идея, делая потоковую передачу мгновенно, пока у вас не начнутся эти ужасные проблемы.
Не то чтобы ваш конкретный случай не был красной селедкой для опытного Хаскеллера: это пример из учебника о том, почему ленивый ввод-вывод является проблемой.
main = do xs <- withFile "test.txt" ReadMode getlines
sequence_ $ map putStrLn xs
withFile принимает FilePath, режим и действие для дескриптора, возникающего в результате открытия этого пути к файлу в этом режиме. Интересной особенностью withFile является то, что он реализован с использованием скобок и гарантирует, даже в случае исключения, что файл будет закрыт после выполнения действия с дескриптором. Проблема здесь в том, что рассматриваемое действие (getLines) вообще не читает файл! Обещаю делать это только тогда, когда контент действительно нужен! Это ленивый ввод-вывод (реализованный с unsafeInterleaveIO, угадайте, что означает "небезопасная" часть...). Конечно, к моменту, когда этот контент необходим (putStrLn), дескриптор был закрыт с помощью FileFile, как и было обещано.
Таким образом, у вас есть несколько решений: вы можете использовать открытое и закрытое явно (и отказаться от исключительной безопасности), или вы можете использовать ленивый ввод-вывод, но каждое действие касаться содержимого файла в области, защищенной withFile:
main = withFile "test.txt" ReadMode$ \h -> do
xs <- getlines h
mapM_ putStrLn xs
В этом случае это не так уж и страшно, но вы должны заметить, что проблема может стать более раздражающей, если вы игнорируете, когда потребуется контент. Ленивый ввод-вывод в большой и сложной программе может быстро стать довольно раздражающим, и когда начинают иметь значение дальнейшие ограничения на число открытых дескрипторов... Именно поэтому новый вид спорта сообщества Haskell состоит в том, чтобы найти решение проблемы потокового контента (вместо чтения целых файлов в памяти, которые "решают" проблему за счет использования раздутой памяти до порой невозможных уровней) без ленивого ввода-вывода. Какое-то время казалось, что Iteratee собирается стать стандартным решением, но оно было очень сложным и трудным для понимания, даже для опытного Хаскеллера, поэтому в последнее время подкрались другие кандидаты: наиболее перспективным или по крайней мере успешным в настоящее время кажется быть "проводником".
Как отметили другие, hGetContents
ленивый Однако вы можете добавить строгости, если захотите:
import Control.DeepSeq
forceM :: (NFData a, Monad m) => m a -> m a
forceM m = do
val <- m
return $!! val
main = do xs <- withFile "text.txt" ReadMode (forceM . getlines)
...
Хотя обычно рекомендуется выполнять все операции ввода-вывода, связанные с содержимым файла внутри withFile
блок вместо. Таким образом, ваша программа может на самом деле воспользоваться отложенным чтением файла, сохраняя в памяти только столько, сколько необходимо. Если вы имеете дело с очень большим файлом, то принуждение всего файла к считыванию в память обычно является плохой идеей.
Если вам нужен более детальный контроль над ресурсами, то вам следует рассмотреть возможность использования ResourceT
(который идет с пакетом трубопровода) или подобный.
[править: использовать $!!
от Control.DeepSeq
(вместо $!
), чтобы убедиться, что все значение принудительно. Спасибо за совет, @benmachine]