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.
We could actually code this as an arrow (Kleisli transformer).
Only, that would violate the arrow laws (you can't rewrite
a *** b
tofirst a >>> second b
without losinga
's error information). But if you basically see all theLeft
s as merely a debugging device, you might argue it's okay.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:
TheseT
monad transformerDisclaimer:
TheseT
is calledChronicleT
inthese
package.Take a look at the definition of
These
data type:Here
This
andThat
correspond toLeft
andRight
ofEither
data type.These
data constructor is what enables accumulating capability forMonad
instance: it contains both result (of typeb
) and a collection of previous errors (collection of typea
).Taking advantage of already existing definition of
These
data type we can easily createErrorT
-like monad transformer:TheseT
is an instance ofMonad
in the following way:Applicative
accumulatingErrorT
Disclaimer: this approach is somewhat easier to adapt since you already work in
m (Either e a)
newtype wrapper, but it works only inApplicative
setting.If the actual code only uses
Applicative
interface we can get away withErrorT
changing itsApplicative
instance.Let's start with a non-transformer version:
Note that when defining
<*>
we know if both sides areALeft
s and thus can perform<>
. If we try to define correspondingMonad
instance we fail:So the only
Monad
instance we might have is that ofEither
. But thenap
is not the same as<*>
:So we only can use
Accum
asApplicative
.Now we can define
Applicative
transformer based onAccum
:Note that
AccErrorT e m
is essentiallyCompose m (Accum e)
.EDIT:
AccError
is known asAccValidation
invalidation
package.