Чтение файлов со ссылками на другие файлы в 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 в потребителя. Смотрите также Есть ли лучший способ пройтись по дереву каталогов?

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