用EitherT累积错误 [英] Accumulating errors with EitherT
问题描述
我有以下一个小型样例应用的一个web API,它需要一个巨大的JSON文档,并且应该分析它并分析每个部分的错误消息。
以下代码是使用EitherT(和错误包)的工作示例。然而,问题是EitherT打破了第一个左边的计算,只是返回它看到的第一个错误。我想要的是一个错误消息的列表,所有这些都是可能的。例如,如果 runEitherT
中的第一行失败,那么没有什么可以做的。但是如果第二行失败,那么我们仍然可以尝试运行后续行,因为它们在第二行没有数据依赖关系。所以我们理论上可以一次性生成(不一定全部)错误消息。
是否可以懒惰地运行所有的计算并返回我们可以找到所有的错误信息?
{ - #LANGUAGE OverloadedStrings# - }
模块Main
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作为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
派生(显示)
数据TypeTwo = TypeTwo Double
派生(显示)
数据TypeThree = TypeThree Double
派生(显示)
main :: IO()
main = scotty 3000 $ do
中间件logStdoutDev
post/ pdor$ do
api_key< - paramapi_key
input< - paraminput
typeOne< - runEitherT $ do
result< - (decode(pack input):: Maybe Value)? 无法解析输入JSON文件格式错误
typeTwoObj< - (result ^?keytypeTwo)? 在JSON文件中找不到关键字两个。
typeThreeObj< - (result ^?keytypeThree)? 在JSON文档中找不到关键类型三。
name< - (result ^?keyname。_String)? 无法在JSON文档中找到密钥名称。
typeTwo< - hoistEither $ prependLeft解析TypeTwo时出错:$ parseEither jsonTypeTwo typeTwoObj
typeThree< - hoistEither $ prependLeft解析TypeThree时出错:$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ b return $ TypeOne name typeTwo typeThree
case type
的一个left errorMsg - > do
_< - status badRequest400
S.json $ object [error。= errorMsg]
右_ - >
- 用解析的Haskell类型
S.json $ object [api_key。=(api_key :: String),message。=(success:: String)]
prependLeft :: String - >字符串a - >任何一个字符串a
prependLeft msg(Left s)= Left(msg ++ s)
prependLeft _ x = x
jsonTypeTwo :: Value - >解析器TypeTwo
jsonTypeTwo(Object v)= TypeTwo< $> v。:val
jsonTypeTwo _ = fail $TypeTwo没有数据
jsonTypeThree :: Value - >解析器TypeThree
jsonTypeThree(Object v)= TypeThree< $> v。:val
jsonTypeThree _ = fail $TypeThree没有数据
如果任何人有一些,也可以重构建议。
正如我在评论中提到的那样,您至少有两种累积错误的方法。下面我详细说明那些。我们需要这些导入:
import Control.Applicative
import Data.Monoid
import Data .se
TheseT
monad transformer
免责声明: TheseT
被称为 ChronicleT
在 这些
包。
查看 这些
数据类型:
data这些ab = This a |那个b |这些ab
这里这个
和该
对应于左
和右
数据类型。 这些
数据构造函数是能够为 Monad
实例的累积功能:它包含两个结果(类型为$ code > b )和以前错误的集合( a
的集合)。
利用已经存在的定义这些
数据类型,我们可以轻松地创建 ErrorT
类似的monad变换器:
newtype TheseT ema = TheseT {
runTheseT :: m(These ea)
}
TheseT
是 Monad
以下方式:
实例Functor m => Functor(TheseT e m)其中
fmap f(TheseT m)= TheseT(fmap(fmap f)m)
instance(Monoid e,Applicative m)=>其中
pure x = TheseT(pure(pure x))
TheseT f *这些T x = TheseT(liftA2(*))f x)
实例(Monoid e,Monad m)=> Monad(TheseT em)其中
返回x = TheseT(return(return x))
m>> = f = TheseT $ do
t< - runTheseT m
case t的
这个e - > return(this e)
那个x - > runTheseT(f x)
这些_ x - > do
t'< - runTheseT(fx)
return(t>> t') - 这是错误连接的地方
适用
累积 ErrorT
免责声明:这种方法更容易适应,因为您已经在 m(EEA)
newtype wrapper ,但它只适用于适用
设置。
如果实际代码只使用
接口我们可以通过 ErrorT
更改其应用
实例。
我们从非变压器版本开始:
data Accum ea = ALeft e | ARight a
实例Functor(Accum e)其中
fmap f(ARight x)= ARight(fx)
fmap _(ALeft e)= ALeft e
instance Monoid e =>适用(Accum e)其中
pure = ARight
ARight f * ARight x = ARight(f x)
ALeft e * ALeft e'= ALeft(e e')
ALeft e * _ = ALeft e
_< *>注意,当定义< *> / code>我们知道如果双方都是 ALeft
,因此可以执行<> code>。如果我们尝试定义相应的 Monad
实例,我们将失败: instance Monoid e => Monad(Accum e)其中
return = ARight
ALeft e>> = f = - 我们不能应用f
所以我们可能只有 Monad
实例是或者
。但是 ap
与< *>
不同:
留下< *>左b≡左(a< b)
留下一个`ap`左b≡左a
所以我们只能使用 Accum
作为适用
。
现在我们可以根据 Accum
定义应用
变压器:
newtype AccErrorT ema = AccErrorT {
runAccErrorT :: m(Accum ea)
}
实例(Functor m )=> Functor(AccErrorT e m)其中
fmap f(AccErrorT m)= AccErrorT(fmap(fmap f)m)
instance(Monoid e,Applicative m)=>应用(AccErrorT e m)其中
pure x = AccErrorT(pure(pure x))
AccErrorT f * AccErrorT x = AccErrorT(liftA2(*)fx)
请注意, AccErrorT em
基本上是 Compose m(Accum e)
。
编辑:
AccError
被称为 AccValidation
在 验证
包 。
I have the following little mini-sample application of a web API that takes a huge JSON document and is supposed to parse it in pieces and report error messages for each of the pieces.
Following code is a working example of that using EitherT (and the errors package). However, the problem is that EitherT breaks the computation on the first Left encountered and just returns the first "error" it sees. What I would like is a list of error messages, all that are possible to produce. For instance, if the first line in runEitherT
fails then there's nothing more that can be done. But if the second line fails then we can still try to run subsequent lines because they have no data dependency on the second line. So we could theoretically produce more (not necessarily all) error messages in one go.
Is it possible to run all the computations lazily and return all the error messages we can find out?
{-# 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"
Also open to refactoring suggestions if anyone has some.
解决方案 As I mentioned in a comment, you have at least 2 ways of accumulating error. Below I elaborate on those. We'll need these imports:
import Control.Applicative
import Data.Monoid
import Data.These
TheseT
monad transformer
Disclaimer: TheseT
is called ChronicleT
in these
package.
Take a look at the definition of These
data type:
data These a b = This a | That b | These a b
Here This
and That
correspond to Left
and Right
of Either
data type. These
data constructor is what enables accumulating capability for Monad
instance: it contains both result (of type b
) and a collection of previous errors (collection of type a
).
Taking advantage of already existing definition of These
data type we can easily create ErrorT
-like monad transformer:
newtype TheseT e m a = TheseT {
runTheseT :: m (These e a)
}
TheseT
is an instance of Monad
in the following way:
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
accumulating ErrorT
Disclaimer: this approach is somewhat easier to adapt since you already work in m (Either e a)
newtype wrapper, but it works only in Applicative
setting.
If the actual code only uses Applicative
interface we can get away with ErrorT
changing its Applicative
instance.
Let's start with a non-transformer version:
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
Note that when defining <*>
we know if both sides are ALeft
s and thus can perform <>
. If we try to define corresponding Monad
instance we fail:
instance Monoid e => Monad (Accum e) where
return = ARight
ALeft e >>= f = -- we can't apply f
So the only Monad
instance we might have is that of Either
. But then ap
is not the same as <*>
:
Left a <*> Left b ≡ Left (a <> b)
Left a `ap` Left b ≡ Left a
So we only can use Accum
as Applicative
.
Now we can define Applicative
transformer based on Accum
:
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)
Note that AccErrorT e m
is essentially Compose m (Accum e)
.
EDIT:
AccError
is known as AccValidation
in validation
package.
这篇关于用EitherT累积错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!