簡體   English   中英

管道中的錯誤處理

[英]Error handling in pipes

背景故事

我有許多數據文件,每個文件都包含一個數據記錄列表(每行一個)。 與 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 ErrMsgP.mapM的 monad 實例而不是P.map 由於我們正在讀取一個文件,我們的 monad 堆棧中已經有IOSafeT ,因此,我想我需要用ExceptT來處理該 monad 堆棧中的錯誤。 這是我卡住的地方。 我嘗試了許多不同的組合,結果總是被類型檢查員大吼大叫。 以下是我能得到的最接近它的 compiles

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 中的另一個動機是,如果我不必在我的地圖和折疊中處理任何一個,它將使進一步的數據處理變得更加容易。

這是解決問題的增量方法。

按照 Tekmo 在此 SO 答案中的建議,我們的目標是在以下 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 的 SO 回答建議為此創建一個新的運營商。

最后,我們可以使用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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM