Чтение файлов со ссылками на другие файлы в haskell
Я пытаюсь расширить обычную уценку с возможностью иметь ссылки на другие файлы, так что содержимое в ссылочных файлах отображается в соответствующих местах в "главном" файле.
Но самое дальнее, что я пришел, это реализовать
createF :: FTree -> IO String
createF Null = return ""
createF (Node f children) = ifNExists f (_id f)
(do childStrings <- mapM createF children
withFile (_path f) ReadMode $ \handle ->
do fc <- lines <$> hGetContents handle
return $ merge fc childStrings)
ifNExists
это просто помощник, который можно игнорировать, реальная проблема возникает при чтении дескриптора, он просто возвращает пустую строку, я полагаю, это из-за ленивого ввода-вывода.
Я думал, что использование withFile filepath ReadMode $ \handle -> {-do stutff-}hGetContents handle
было бы правильным решением, как я прочитал fcontent <- withFile filepath ReadMode hGetContents
плохая идея
Еще одна вещь, которая меня смущает, это то, что функция
createFT :: File -> IO FTree
createFT f = ifNExists f Null
(withFile (_path f) ReadMode $ \handle ->
do let thisParse = fparse (_id f :_parents f)
children <-rights . map ( thisParse . trim) . lines <$> hGetContents handle
c <- mapM createFT children
return $ Node f c)
работает как шарм.
Так почему же createF
вернуть только пустую строку?
весь проект и каталог / файл для тестирования можно найти на github
Вот определения типов данных
type ID = String
data File = File {_id :: ID, _path :: FilePath, _parents :: [ID]}
deriving (Show)
data FTree = Null
| Node { _file :: File
, _children :: [FTree]} deriving (Show)
2 ответа
Как вы и подозревали, проблема в ленивом IO. Вот (ужасное) правило, которому вы должны следовать, чтобы правильно его использовать, не сходя с ума:
withFile
вычисления не должны завершаться до тех пор, пока не будут выполнены все (ленивые) операции ввода-вывода, необходимые для полной оценки его результата.
Если что-то вынуждает ввод / вывод после закрытия дескриптора, вы не гарантированно получите ошибку, даже если это будет очень приятно. Вместо этого вы получаете совершенно неопределенное поведение.
Вы нарушаете это правило с return $ merge fc childStrings
потому что это значение возвращается до того, как оно будет полностью оценено. То, что вы можете сделать вместо этого, является чем-то неопределенным
let retVal = merge fc childStrings
deepSeq retVal $ return retVal
Возможно, более чистая альтернатива - поместить весь остальной код, который опирается на эти результаты, в withFile
аргумент. Единственная реальная причина этого не делать, если вы выполняете кучу другой работы с результатами после того, как закончите работу с этим файлом. Например, если вы обрабатываете кучу разных файлов и накапливаете их результаты, то вы должны быть уверены, что закрыли каждый из них, когда закончили с этим. Если вы просто читаете один файл, а затем воздействуете на него, вы можете оставить его открытым, пока не закончите.
Кстати, я только что отправил запрос на добавление функций в группу GHC, чтобы узнать, не захотят ли они сделать такие программы более ранними с ошибками с полезными сообщениями об ошибках.
Обновить
Запрос на функцию был принят, и такие программы теперь с большей вероятностью выдают полезные сообщения об ошибках. См. Что вызвало эту ошибку "отложенное чтение по закрытому дескриптору"? для деталей.
Я настоятельно рекомендую вам избегать ленивых операций ввода-вывода, поскольку они всегда создают такие проблемы, как описано в разделе "Что плохого в ленивых операциях ввода-вывода?". Как и в вашем случае, когда вам нужно держать файл открытым, пока он не будет полностью прочитан, но это будет означать закрытие файла где-то в чистом коде, когда содержимое фактически используется.
Одна возможность будет использовать строгий ByteString
и читать файлы с помощью readFile
, Это также сделает многие операции более эффективными.
Другой вариант - использовать одну из библиотек, которые решают проблему отложенного ввода-вывода (см. " Каковы плюсы и минусы счетчиков против проводников против труб"). Эти библиотеки позволяют отделить производство контента от его обработки или потребления. Таким образом, вы можете иметь производителя, который читает входные файлы и создает поток некоторых токенов, и чистого потребителя (не зависимо от IO
), который потребляет поток и дает некоторый результат. Например, в Wire-Extra есть модуль, который преобразует синтаксический анализатор atto-parsec в потребителя. Смотрите также Есть ли лучший способ пройтись по дереву каталогов?