将类型与 ExceptT IO monad 转换器对齐

Align types with ExceptT IO monad transformer

试图将我的头脑围绕在 monad 变换器上,我可以得到一些玩具示例来工作,但在这里我正在努力处理一个更真实的用例。在 的基础上,使用 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,那么如果使用 Either.

,那么助手 return 相当于 Left $ Error "message"

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

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

我尝试使用各种函数代替 runExceptT,即 runExceptwithExceptwithExceptT,但其中 none 有效。我可以在 ExpectedActual 类型之间得到的闭包是 runExceptT.

为了 retrieveValues 编译并正确地 return “或者” ErrorText 形式的结果应该改变什么?

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

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

仔细考虑您的各种 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 IO monad 中,这意味着顶部 do 块中的每个语句都需要位于该 monad 中。但是,您的第一个语句 eitherResult <- runExceptT $ do ... 并不存在。 runExceptT 的类型是 ExceptT e m a -> m (Either e a),在本例中特化为 ExceptT Error IO Text -> IO (Either Error Text),这意味着它存在于 IO monad 中, 而不是 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 块是类型 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 根本不改变程序。)