WithFile vs. 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

Подставляя File для 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

Ответ 1

Файл закрывается слишком рано. Из документа :

Ручка будет закрыта при выходе из withFile

Это означает, что файл будет закрыт, как только функция withFile вернется.

Поскольку hGetContents и друзья ленивы, он не будет пытаться прочитать файл, пока он не будет принудительно с putStrLn, но к тому времени withFile уже закрыл файл.

Чтобы решить проблему, передайте все это withFile:

main = withFile "test.txt" ReadMode $ \handle -> do
           xs <- getlines handle
           sequence_ $ map putStrLn xs

Это работает, потому что к моменту времени withFile приближается к закрытию файла, вы уже напечатали его.

Ответ 2

Ух, никто не давал простого решения?

main :: IO ()
main = do xs <- fmap lines $ readFile "test.txt"
          mapM_ putStrLn xs

Не используйте openFile + hGetContents или withFile + hGetContents, когда вы можете просто использовать readFile. С помощью readFile вы не можете стрелять в ногу, закрыв файл слишком рано.

Ответ 3

Они делают совершенно разные вещи. 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 ленив, он не будет читать какой-либо вывод, пока файл открыт, и как только файл будет закрыт, он не сможет.

Ответ 4

Вы столкнулись с обычными препятствиями ленивого IO... ленивый IO звучит как отличная идея, создавая потоковое воспроизведение, пока вы не начнете получать эти ужасные проблемы.

Не то, чтобы ваш конкретный случай не был красной серой для опытного Haskeller: это пример учебника о том, почему ленивый IO является проблемой.

main = do xs <- withFile "test.txt" ReadMode getlines
          sequence_ $ map putStrLn xs

withFile принимает FilePath, режим и действие, выполняемое с дескриптором в результате открытия этого пути к файлу в этом режиме. Интересная часть fromFile заключается в том, что она реализована с помощью скобки и гарантирует, даже в случае исключения, что файл будет закрыт после выполнения действия на дескрипторе. Проблема здесь в том, что действие, о котором идет речь (getLines), вообще не читает файл! Это только обещание сделать это, когда контент действительно необходим! Это ленивый IO (реализованный с unsafeInterleaveIO, угадайте, что означает "небезопасная" часть...). Конечно, к тому времени, когда это содержимое необходимо (putStrLn), дескриптор был закрыт с помощью файла, как и было обещано.

Итак, у вас есть несколько решений: вы можете открыто и закрывать явным образом (и отказаться от безопасности исключений), или вы можете использовать ленивый ввод-вывод, но все действия касаются содержимого файла в области, защищенной с помощью файла:

main = withFile "test.txt" ReadMode$ \h -> do
         xs <- getlines h
         mapM_ putStrLn xs

В этом случае это не слишком ужасно, но вы должны увидеть, что проблема может стать более раздражающей, если вы проигнорируете, когда потребуется контент. Lazy IO в большой и сложной программе может быстро стать довольно раздражающей, и когда дальнейшее ограничение количества открытых дескрипторов начинает иметь значение... Вот почему новый вид спорта сообщества Haskell заключается в том, чтобы придумать решения проблемы потокового контента (вместо того, чтобы читать целые файлы в памяти, которые "решают" проблему ценой раздувающейся памяти, иногда до невозможных уровней) без ленивого ввода-вывода. Какое-то время казалось, что Iteratee станет стандартным решением, но это было очень сложно и трудно понять даже для опытного Haskeller, поэтому в последнее время подкрались другие кандидаты: наиболее перспективные или, по крайней мере, успешные в настоящее время, похоже, be "кабелепровод" .

Ответ 5

Как отмечали другие, 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)
          ...

Хотя обычно рекомендуется выполнять все IO, связанные с содержимым файла внутри блока withFile. Таким образом, ваша программа может фактически использовать чтение ленивого файла, сохраняя только столько, сколько необходимо в памяти. Если вы имеете дело с очень большим файлом, то принудительное чтение всего файла в память обычно плохое.

Если вам нужен более мелкомасштабный контроль ресурсов, вам следует изучить ResourceT (который поставляется с conduit пакет) или аналогичный.

[edit: используйте $!! от Control.DeepSeq (вместо $!), чтобы убедиться, что все значение принудительно. Спасибо за подсказку, @benmachine]