【问题标题】:Align types with ExceptT IO monad transformer将类型与 exceptT IO monad 转换器对齐
【发布时间】:2021-05-23 14:50:24
【问题描述】:

试图将我的头包裹在 monad 转换器上,我可以得到一些玩具示例来工作,但在这里我正在努力解决一个稍微更真实的用例。以this previous question 为基础,使用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,则帮助器返回与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,但它们都不起作用。我可以在ExpectedActual 类型之间获得的封闭是runExceptT

为了使retrieveValues 编译并正确返回“要么”ErrorText 形式的结果,应该进行哪些更改?

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

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

【问题讨论】:

  • 你根本不需要runExceptTretrieveValues 函数声明的返回类型是 ExceptT,而不是 IO (Either Error Text)
  • 现在我觉得很尴尬:)
  • @4castle 感谢这个(洞察力非常明显)的答案。这可能是这个示例事物与我正在尝试构建的真实世界事物的更大背景之间存在很大混淆的结果。现在这完美无缺。

标签: haskell monads monad-transformers


【解决方案1】:

仔细考虑你的各种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 &lt;- runExceptT $ do ... 并不存在。 runExceptT 的类型是ExceptT e m a -&gt; m (Either e a),在这种情况下专门用于ExceptT Error IO Text -&gt; 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 而无需更改程序完全没有。)

【讨论】:

    猜你喜欢
    • 2021-11-27
    • 2017-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-29
    • 1970-01-01
    • 1970-01-01
    • 2013-03-16
    相关资源
    最近更新 更多