我有许多数据文件,每个文件都包含一个数据记录列表(每行一个)。 与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
来转置 eithers列表。
readNumbers2 :: IO (Either ErrMsg [Integer])
readNumbers2 = sequence <$> readNumbers1
但是,即使第一行已经格式错误,也会读取整个文件。 这些文件可能很大,而且我有很多文件,因此,如果读取在第一次错误时停止会更好。
我的问题是如何实现这一目标的。 如何在第一个格式错误的记录上中止解析?
我的第一个想法是使用Either ErrMsg
和P.mapM
的monad实例而不是P.map
。
由于我们正在读取文件,因此我们的monad堆栈中已经有IO
和SafeT
,因此,我想我需要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中的另一个动机是,如果我不必在地图和折叠中使用eithers,那么它将使得进一步的数据处理变得更加容易。
答案 0 :(得分:1)
这是解决问题的渐进方法。
根据Tekmo的建议this SO answer 我们的目标是在以下monad中运作:
ExceptT String (Pipe a b m) r
我们从导入和parseNumber
的定义开始:
import Control.Monad.Except
import Pipes ((>->))
import qualified Pipes as P
import qualified Pipes.Prelude as P
parseNumber :: String -> Either String Integer
parseNumber s = case reads s of
[(n, "")] -> Right n
_ -> Left $ "Parse Error: \"" ++ s ++ "\""
这是IO-monad中一个简单的字符串生产者,我们将用作输入:
p1 :: P.Producer String IO ()
p1 = P.stdinLn >-> P.takeWhile (/= "quit")
要将其提升为ExceptT monad,我们只使用lift
:
p2 :: ExceptT String (P.Producer String IO) ()
p2 = lift p1
这是一个管道段,它将字符串转换为ExceptT monad中的整数:
p4 :: ExceptT String (P.Pipe String Integer IO) a
p4 = forever $
do s <- lift P.await
case parseNumber s of
Left e -> throwError e
Right n -> lift $ P.yield n
可能可以更多地组合编写,但为了清晰起见,我已经非常清楚地说明了这一点。
接下来我们一起加入p2和p4。结果也在ExceptT monad中。
-- join together p2 and p4
p7 :: ExceptT String (P.Producer Integer IO) ()
p7 = ExceptT $ runExceptT p2 >-> runExceptT p4
Tekmo的答案建议为此创建一个新的运算符。
最后,我们可以使用toListM'
来运行此管道。 (我在这里包含了toListM'
的定义,因为它没有出现在我安装的Pipes.Prelude版本中)
p8 :: IO ([Integer], Either String ())
p8 = toListM' $ runExceptT p7
toListM' :: Monad m => P.Producer a m r -> m ([a], r)
toListM' = P.fold' step begin done
where
step x a = x . (a:)
begin = id
done x = x []
p8的工作原理示例:
ghci> p8
4
5
6
quit
([4,5,6],Right ())
ghci> p8
5
asd
([5],Left "Parse Error: \"asd\"")
<强>更新强>
您可以通过概括parseNumber
来简化代码:
parseNumber' :: (MonadError [Char] m) => String -> m Integer
parseNumber' s = case reads s of
[(n, "")] -> return n
_ -> throwError $ "Parse Error: \"" ++ s ++ "\""
然后可以写p4
:
p4' :: ExceptT String (P.Pipe String Integer IO) a
p4' = forever $ lift P.await >>= parseNumber' >>= lift . P.yield