Trying to wrap my head around monad transformers, I could get some toy examples to work but here I'm struggling with a slightly more real-worldy use case. Building upon this previous question, with a more realistic example using ExceptT
, where three helper functions are defined.
{-# LANGUAGE RecordWildCards #-}
-- imports so that the example is reproducible
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans.Except
import qualified Data.List as L
import Data.Text (Text)
import qualified Data.Text as T
import System.Random (Random (randomRIO))
-- a few type declarations so the example is easier to follow
newtype Error = Error Text deriving Show
newtype SQLQuery = SQLQuery Text deriving Show
newtype Name = Name { unName :: Text } deriving Show
data WithVersion = WithVersion { vName :: Name, vVersion :: Int }
-- | for each name, retrieve the corresponding version from an external data store
retrieveVersions :: [Name] -> ExceptT Error IO [WithVersion]
retrieveVersions names = do
doError <- liftIO $ randomRIO (True, False) -- simulate an error
if doError
then throwE $ Error "could not retrieve versions"
else do
let fv = zipWith WithVersion names [1..] -- just a simulation
pure fv
-- | construct a SQL query based on the names/versions provided
-- (note that this example is a toy with a fake query)
mkQuery :: [WithVersion] -> SQLQuery
mkQuery withVersions =
SQLQuery $ mconcat $ L.intersperse "\n" $ (\WithVersion {..} ->
unName vName <> ":" <> T.pack (show vVersion)
) <$> withVersion
-- | query an external SQL database and return result as Text
queryDB :: SQLQuery -> ExceptT Error IO Text
queryDB q = do
doError <- liftIO $ randomRIO (True, False) -- simulate an error
if doError
then throwE $ Error "SQL error"
else do
pure "This is the result of the (successful) query"
The calls to randomRIO
are there to simulate the possibility of an error. If doError
is True
, then the helper returns what would be the equivalent of Left $ Error "message"
if using Either
.
All the helpers above compile just fine, however the example wrapper function below doesn't compile:
-- | given a list of names, retrieve versions, build query and retrieve result
retrieveValues :: [Name] -> ExceptT Error IO Text
retrieveValues names = do
eitherResult <- runExceptT $ do
withVersions <- retrieveVersions names
let query = mkQuery withVersions
queryDB query
case eitherResult of
Left err -> throwE err
Right result -> pure result
The error given by GHC is as follows:
• Couldn't match type ‘IO’ with ‘ExceptT Error IO’
Expected type: ExceptT Error IO (Either Error Text)
Actual type: IO (Either Error Text)
• In a stmt of a 'do' block:
eitherResult <- runExceptT
$ do withVersions <- retrieveVersions names
let query = mkQuery withVersions
queryDB query
I've tried using various functions in place of runExceptT
, namely runExcept
, withExcept
, withExceptT
but none of them worked. The closed I can get between Expected
and Actual
types is with runExceptT
.
What should be changed in order for retrieveValues
to compile and properly return "either" an Error
or a result in the form of a Text
?
I also think that using caseEitherResult of
might be redundant here because all it does is essentially passing on either the result or the error, with no additional processing, so I attempted a more direct version, which also fails:
retrieveValues :: [Name] -> ExceptT Error IO Text
retrieveValues names = do
runExceptT $ do
withVersions <- retrieveVersions names
let query = mkQuery withVersions
queryDB query
runExceptT
at all. The declared return type of yourretrieveValues
function is anExceptT
, not anIO (Either Error Text)
– 4castle