Предыстория
У меня есть несколько файлов данных, каждый из которых содержит список записей данных (по одному на строку). Подобно CSV, но достаточно отличается, чтобы я предпочел написать свой собственный анализатор, а не использовать библиотеку CSV. Для этого вопроса я буду использовать упрощенный файл данных, содержащий только одно число в строке:
1
2
3
error
4
Как видите, возможно, что файл содержит искаженные данные, и в этом случае весь файл следует считать искаженным.
Тип обработки данных, который я хочу выполнить, можно выразить в виде карт и складок. Итак, я подумал, что это хорошая возможность научиться пользоваться библиотекой pipes
.
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.Except
import Pipes ((>->))
import qualified Pipes as P
import qualified Pipes.Prelude as P
import qualified Pipes.Safe as P
import qualified System.IO as IO
Сначала я создаю производителя строк в текстовом файле. Это очень похоже на пример в документации Pipes.Safe
.
getLines = do
P.bracket (IO.openFile "data.txt" IO.ReadMode) IO.hClose P.fromHandle
Затем мне нужна функция для анализа каждой из этих строк. Как я упоминал ранее, это может привести к сбою, что я представлю с помощью Either
.
type ErrMsg = String
parseNumber :: String -> Either ErrMsg Integer
parseNumber s = case reads s of
[(n, "")] -> Right n
_ -> Left $ "Parse Error: \"" ++ s ++ "\""
Для простоты в качестве первого шага я хочу собрать все записи данных в список записей. Самый простой подход - пропустить все строки через синтаксический анализатор и просто собрать все это в список.
readNumbers1 :: IO [Either ErrMsg Integer]
readNumbers1 = P.runSafeT $ P.toListM $
getLines >-> P.map parseNumber
К сожалению, это создает список любой из записей. Однако, если файл содержит одну неправильную запись, то весь файл следует считать неправильным. Что мне действительно нужно, так это список записей. Конечно, я могу просто использовать sequence
для транспонирования списка любого из них.
readNumbers2 :: IO (Either ErrMsg [Integer])
readNumbers2 = sequence <$> readNumbers1
Но при этом будет прочитан весь файл, даже если первая строка уже искажена. Эти файлы могут быть большими, и у меня их много, поэтому было бы лучше, если бы чтение остановилось при первой ошибке.
Вопрос
Мой вопрос - как этого добиться. Как отменить синтаксический анализ первой некорректной записи?
Что я получил до сих пор
Моей первой мыслью было использовать экземпляр монады Either ErrMsg
и P.mapM
вместо P.map
. Поскольку мы читаем из файла, у нас уже есть IO
и SafeT
в нашем стеке монад, поэтому, думаю, мне понадобится ExceptT
, чтобы добавить обработку ошибок в этот стек монад. Это тот момент, в котором я застрял. Я пробовал много разных комбинаций и всегда получал крики от проверяющего типа. Следующее - самое близкое, что я могу сделать для компиляции.
readNumbers3 = P.runSafeT $ runExceptT $ P.toListM $
getLines >-> P.mapM (ExceptT . return . parseNumber)
Введенный тип readNumbers3
читает
*Main> :t readNumbers3
readNumbers3
:: (MonadIO m, P.MonadSafe (ExceptT ErrMsg (P.SafeT m)),
P.MonadMask m, P.Base (ExceptT ErrMsg (P.SafeT m)) ~ IO) =>
m (Either ErrMsg [Integer])
что похоже на то, что я хочу:
readNumbers3 :: IO (Either ErrMsg [Integer])
Однако как только я пытаюсь выполнить это действие, я получаю следующее сообщение об ошибке в ghci:
*Main> readNumbers3
<interactive>:7:1:
Couldn't match expected type ‘IO’
with actual type ‘P.Base (ExceptT ErrMsg (P.SafeT m0))’
The type variable ‘m0’ is ambiguous
In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
Если я попытаюсь применить следующую подпись типа:
readNumbers3 :: IO (Either ErrMsg [Integer])
Затем я получаю следующее сообщение об ошибке:
error.hs:108:5:
Couldn't match expected type ‘IO’
with actual type ‘P.Base (ExceptT ErrMsg (P.SafeT IO))’
In the first argument of ‘(>->)’, namely ‘getLines’
In the second argument of ‘($)’, namely
‘getLines >-> P.mapM (ExceptT . return . parseNumber)’
In the second argument of ‘($)’, namely
‘P.toListM $ getLines >-> P.mapM (ExceptT . return . parseNumber)’
Failed, modules loaded: none.
В стороне
Еще одна мотивация для переноса обработки ошибок в базовую монаду канала заключается в том, что это значительно упростило бы дальнейшую обработку данных, если бы мне не пришлось жонглировать любым из них в моих картах и свертках.