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 transformerDisclaimer: 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With