首页 > 解决方案 > 将类型与 exceptT IO monad 转换器对齐

问题描述

试图将我的头包裹在 monad 转换器上,我可以得到一些玩具示例来工作,但在这里我正在努力解决一个稍微更真实的用例。建立在上一个问题的基础上,使用更实际的示例 using ExceptT,其中定义了三个辅助函数。

{-# 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"

调用randomRIO是为了模拟错误的可能性。如果doErrorTrue,则帮助器返回相当于Left $ Error "message"if using的内容Either

上面的所有帮助程序都可以正常编译,但是下面的示例包装函数无法编译:

-- | 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

GHC给出的错误如下:

• 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

我尝试过使用各种功能来代替,runExceptTrunExcept,但它们都不起作用。我可以在和类型之间得到的封闭是 with 。withExceptwithExceptTExpectedActualrunExceptT

retrieveValues为了编译并正确返回“要么”anError或a形式的结果,应该改变什么Text

我也认为 usingcaseEitherResult of在这里可能是多余的,因为它所做的只是传递结果或错误,没有额外的处理,所以我尝试了一个更直接的版本,它也失败了:

retrieveValues :: [Name] -> ExceptT Error IO Text
retrieveValues names = do
    runExceptT $ do
        withVersions <- retrieveVersions names
        let query = mkQuery withVersions
        queryDB query

标签: haskellmonadsmonad-transformers

解决方案


仔细考虑一下你的各个do块正在使用什么单子。让我们先看看您对 的第一个定义retrieveValues

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

这个函数存在于ExceptT Error IOmonad 中,这意味着顶部do块中的每个语句都需要在该 monad 中。但是,您的第一个语句eitherResult <- runExceptT $ do ...并不存在。的类型runExceptTExceptT e m a -> m (Either e a),在这种情况下专门用于ExceptT Error IO Text -> IO (Either Error Text),这意味着它存在于IO单子中,而不是ExceptT Error IO!要解决此问题,您需要lift得到结果。因此,该行应如下所示:

    eitherResult <- lift $ runExceptT $ do

您的第二个定义也非常接近工作,但是从第一个定义修改它时您没有完全删除。你写了:

retrieveValues :: [Name] -> ExceptT Error IO Text
retrieveValues names = do
    runExceptT $ do
        withVersions <- retrieveVersions names
        let query = mkQuery withVersions
        queryDB query

你应该问自己的问题是:我什至需要第三行吗?换句话说,如果你的结果应该是ExceptT Error IO Text并且你的内部do块是 type ExceptT Error IO Text,那么你为什么要打电话runExceptT呢?或者,也许你的目标是生成一个Either作为这个函数的结果,所以这runExceptT很关键,但现在这个类型没有意义了。换句话说,有两种方法可以解决这个问题。首先,您可以通过删除第三行来修复实现以匹配类型:

retrieveValues :: [Name] -> ExceptT Error IO Text
retrieveValues names = do
    withVersions <- retrieveVersions names
    let query = mkQuery withVersions
    queryDB query

或者,您可以更改类型以匹配实现:

retrieveValues :: [Name] -> IO (Either Error Text)
retrieveValues names = do
    runExceptT $ do
        withVersions <- retrieveVersions names
        let query = mkQuery withVersions
        queryDB query

(通常请注意,do只有一条语句的块根本不需要在do块中。因此在这种情况下,您可以删除第一个语句而根本不do改变程序。)


推荐阅读