【问题标题】:Accumulating errors with EitherT使用 EitherT 累积错误
【发布时间】:2014-05-12 11:00:00
【问题描述】:

我有以下 Web API 的小型示例应用程序,该应用程序需要一个巨大的 JSON 文档,并且应该将其解析为片段并报告每个片段的错误消息。

以下代码是使用 EitherT(和错误包)的工作示例。然而,问题是 EitherT 中断了遇到的第一个 Left 的计算,只返回它看到的第一个“错误”。我想要的是一个错误消息列表,所有这些都是可能产生的。例如,如果runEitherT 中的第一行失败,那么就没有什么可以做的了。但是如果第二行失败,那么我们仍然可以尝试运行后续行,因为它们对第二行没有数据依赖。所以理论上我们可以一次性产生更多(不一定是全部)错误消息。

是否可以懒惰地运行所有的计算并返回我们能找到的所有错误消息?

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Data.ByteString.Lazy.Char8 (pack)
import Web.Scotty as S
import Network.Wai.Middleware.RequestLogger
import Data.Aeson
import Data.Aeson.Types
import Control.Lens hiding ((.=), (??))
import Data.Aeson.Lens
import qualified Data.Text as T
import Control.Error
import Control.Applicative
import qualified Data.HashMap.Strict as H
import Network.HTTP.Types

data TypeOne = TypeOne T.Text TypeTwo TypeThree
  deriving (Show)

data TypeTwo = TypeTwo Double
  deriving (Show)

data TypeThree = TypeThree Double
  deriving (Show)

main :: IO ()
main = scotty 3000 $ do
  middleware logStdoutDev

  post "/pdor" $ do
    api_key <- param "api_key"
    input   <- param "input"

    typeOne <- runEitherT $ do
      result       <- (decode (pack input) :: Maybe Value) ?? "Could not parse. Input JSON document is malformed"
      typeTwoObj   <- (result ^? key "typeTwo")            ?? "Could not find key typeTwo in JSON document."
      typeThreeObj <- (result ^? key "typeThree")          ?? "Could not find key typeThree in JSON document."
      name         <- (result ^? key "name" . _String)     ?? "Could not find key name in JSON document."
      typeTwo      <- hoistEither $ prependLeft "Error when parsing TypeTwo: " $ parseEither jsonTypeTwo typeTwoObj
      typeThree    <- hoistEither $ prependLeft "Error when parsing TypeThree: " $ parseEither jsonTypeThree typeThreeObj

      return $ TypeOne name typeTwo typeThree

    case typeOne of
      Left errorMsg -> do
        _ <- status badRequest400
        S.json $ object ["error" .= errorMsg]
      Right _ ->
        -- do something with the parsed Haskell type
        S.json $ object ["api_key" .= (api_key :: String), "message" .= ("success" :: String)]

prependLeft :: String -> Either String a -> Either String a
prependLeft msg (Left s) = Left (msg ++ s)
prependLeft _ x = x

jsonTypeTwo :: Value -> Parser TypeTwo
jsonTypeTwo (Object v) = TypeTwo <$> v .: "val"
jsonTypeTwo _ = fail $ "no data present for TypeTwo"

jsonTypeThree :: Value -> Parser TypeThree
jsonTypeThree (Object v) = TypeThree <$> v .: "val"
jsonTypeThree _ = fail $ "no data present for TypeThree"

如果有人有重构建议,也可以接受。

【问题讨论】:

  • 错误累积ErrorT 不能成为正确的Monad。只有Applicative。所以你需要一个不同的类型(比如 These 只是一个变压器形式)或一个 newtype 包装器与不同的 Applicative 实例。
  • 一个TheseT 变压器肯定是要走的路!

标签: haskell error-handling either


【解决方案1】:

正如我在评论中提到的,您至少有两种累积错误的方法。下面我详细说明这些。我们需要这些导入:

import Control.Applicative
import Data.Monoid
import Data.These

TheseT monad 变压器

免责声明: TheseTthese package 中被称为 ChronicleT

看看These data type的定义:

data These a b = This a | That b | These a b

这里ThisThat对应Either数据类型的LeftRightThese 数据构造函数为 Monad 实例启用累积能力:它包含结果(b 类型)和先前错误的集合(a 类型的集合)。

利用These 数据类型的现有定义,我们可以轻松创建ErrorT-like monad 转换器:

newtype TheseT e m a = TheseT {
  runTheseT :: m (These e a)
}

TheseTMonad 的一个实例,方式如下:

instance Functor m => Functor (TheseT e m) where
  fmap f (TheseT m) = TheseT (fmap (fmap f) m)

instance (Monoid e, Applicative m) => Applicative (TheseT e m) where
  pure x = TheseT (pure (pure x))
  TheseT f <*> TheseT x = TheseT (liftA2 (<*>) f x)

instance (Monoid e, Monad m) => Monad (TheseT e m) where
  return x = TheseT (return (return x))
  m >>= f = TheseT $ do
    t <- runTheseT m
    case t of
      This  e   -> return (This e)
      That    x -> runTheseT (f x)
      These _ x -> do
        t' <- runTheseT (f x)
        return (t >> t')  -- this is where errors get concatenated

Applicative积累ErrorT

免责声明:这种方法更容易适应,因为您已经在 m (Either e a) newtype 包装器中工作,但它仅适用于 Applicative 设置。

如果实际代码只使用Applicative 接口,我们可以通过ErrorT 更改其Applicative 实例来侥幸。

让我们从非变压器版本开始:

data Accum e a = ALeft e | ARight a

instance Functor (Accum e) where
  fmap f (ARight x) = ARight (f x)
  fmap _ (ALeft e)  = ALeft e

instance Monoid e => Applicative (Accum e) where
  pure = ARight
  ARight f <*> ARight x = ARight (f x)
  ALeft e  <*> ALeft e' = ALeft (e <> e')
  ALeft e  <*> _        = ALeft e
  _        <*> ALeft e  = ALeft e

请注意,在定义&lt;*&gt; 时,我们知道双方是否都是ALefts,因此可以执行&lt;&gt;。如果我们尝试定义相应的Monad 实例,我们会失败:

instance Monoid e => Monad (Accum e) where
  return = ARight
  ALeft e >>= f = -- we can't apply f

所以我们可能拥有的唯一Monad 实例是Either。但是然后ap&lt;*&gt;不一样:

Left a <*>  Left b  ≡  Left (a <> b)
Left a `ap` Left b  ≡  Left a

所以我们只能将Accum 用作Applicative

现在我们可以基于Accum定义Applicative转换器:

newtype AccErrorT e m a = AccErrorT {
  runAccErrorT :: m (Accum e a)
}

instance (Functor m) => Functor (AccErrorT e m) where
  fmap f (AccErrorT m) = AccErrorT (fmap (fmap f) m)

instance (Monoid e, Applicative m) => Applicative (AccErrorT e m) where
  pure x = AccErrorT (pure (pure x))
  AccErrorT f <*> AccErrorT x = AccErrorT (liftA2 (<*>) f x)

请注意,AccErrorT e m 本质上是Compose m (Accum e)


编辑:

AccErrorvalidation package 中称为AccValidation

【讨论】:

  • 感谢类型的实现!我已经根据之前的评论重构了我的代码以使用These,因此只需添加TheseT 的实例有助于使其与之前的实现更加一致。添加一些辅助函数使其看起来与 EitherT 代码非常相似,但现在它按预期累积代码。当然,完成所有这些工作的真正诀窍是提供合理的“默认/空”数据,如果遇到错误,程序可以从中继续计算。代码:gist.github.com/ique/be2af7f274597a55945a
  • @Fredrik 如果TheseT 适合您,也许您应该只使用来自these 包的ChronicleT(如答案中所述)。那不是自产自销的TheseT 变压器。
【解决方案2】:

我们实际上可以将其编码为 箭头(Kleisli 转换器)。

newtype EitherAT x m a b = EitherAT { runEitherAT :: a -> m (Either x b) }

instance Monad m => Category EitherAT x m where
  id = EitherAT $ return . Right
  EitherAT a . EitherAT b
       = EitherAT $ \x -> do
              ax <- a x
              case ax of Right y -> b y
                         Left e  -> return $ Left e

instance (Monad m, Semigroup x) => Arrow EitherAT x m where
  arr f = EitherAT $ return . Right . f
  EitherAT a *** EitherAT b = EitherAT $ \(x,y) -> do
      ax <- a x
      by <- b y
      return $ case (ax,by) of
        (Right x',Right y') -> Right (x',y')
        (Left e  , Left f ) -> Left $ e <> f
        (Left e  , _      ) -> Left e
        (  _     , Left f ) ->        Left f
  first = (***id)

只是,这会违反箭头法则(您不能将a *** b 重写为first a &gt;&gt;&gt; second b 而不会丢失a 的错误信息)。但是,如果您基本上将所有Lefts 视为仅仅是一个调试设备,您可能会认为没关系。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-04-04
    • 1970-01-01
    • 1970-01-01
    • 2014-09-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多