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]

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