背景故事
我有许多数据文件,每个文件都包含一个数据记录列表(每行一个)。与 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
但是,即使第一行已经格式错误,它也会读取整个文件。这些文件可能很大,而且我有很多,所以,如果在第一个错误时停止读取会更好。
问题
我的问题是如何实现这一目标。如何中止解析第一个格式错误的记录?
到目前为止我得到了什么
我的第一个想法是使用 and 的 monad 实例Either ErrMsg
而P.mapM
不是P.map
. 由于我们正在从我们已经拥有的文件中读取,IO
并且SafeT
在我们的 monad 堆栈中,所以,我想我需要ExceptT
在那个 monad 堆栈中进行错误处理。这就是我卡住的地方。我尝试了许多不同的组合,但总是被类型检查员大喊大叫。以下是我能得到的最接近它的编译器。
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.
在旁边
将错误处理移动到管道的基本 monad 的另一个动机是,如果我不必在我的地图和折叠中处理任何一个问题,它将使进一步的数据处理变得更加容易。