如何使用 ExceptT 替换大量 IO(要么 a b)
How to use ExceptT to replace lots of IO (Either a b)
我有一个连接到数据库然后运行查询的函数。这些步骤中的每一步都会导致 IO (Either SomeErrorType SomeResultType)
.
我非常喜欢在学习 Haskell 中使用 Either
和类似 monad 的其中一件事是能够使用像 >>=
这样的 monad 函数和像 mapLeft
以简化对预期错误状态的大量处理。
我在这里阅读博客文章、Control.Monad.Trans
文档和关于 SO 的其他答案的期望是,我必须以某种方式使用转换器/提升从 IO
上下文移动到 Either
上下文。
特别好,但我很难将其应用到我自己的案例中。
我的代码的一个更简单的例子:
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c =
connect c >>= \case
(Left e) -> printErrorAndExit e
(Right conn) -> (run . query id $ conn)
>>= \case
(Left e) -> printErrorAndExit e
(Right r) -> print r
>> release conn
我的问题是 (a) 我并不真正了解 ExceptT
如何让我到达与 mapLeft handleErrors $ eitherErrorOrResult >>= someOtherErrorOrResult >>= print
世界相似的地方的机制; (b) 我不确定如何确保始终以最好的方式释放连接(即使在我上面的简单示例中),尽管我想我会使用 bracket pattern.
我确定每个(相对)新的 Haskeller 都这么说,但我仍然真的不理解 monad 转换器,我读到的所有内容(除了前面链接的 SO 答案)对我来说太不透明了(还) .
如何将上面的代码转换成可以删除所有这些嵌套和错误处理的代码?
我认为查看 ExceptT
的 Monad
实例的源代码很有启发性:
newtype ExceptT e m a = ExceptT (m (Either e a))
instance (Monad m) => Monad (ExceptT e m) where
return a = ExceptT $ return (Right a)
m >>= k = ExceptT $ do
a <- runExceptT m
case a of
Left e -> return (Left e)
Right x -> runExceptT (k x)
如果忽略newtype
的包裹和展开,就更简单了:
m >>= k = do
a <- m
case a of
Left e -> return (Left e)
Right x -> k x
或者,您似乎不喜欢使用 do
:
m >>= k = m >>= \a -> case a of
Left e -> return (Left e)
Right x -> k x
你是不是觉得这段代码很眼熟?那和你的代码之间的唯一区别是你写 printErrorAndExit
而不是 return . Left
!所以,让我们把那个 printErrorAndExit
移到顶层,并且很高兴 记住 现在的错误而不打印它。
simpleVersion :: Integer -> Config -> IO (Either Err ())
simpleVersion id c = connect c >>= \case (Left e) -> return (Left e)
(Right conn) -> (run . query id $ conn)
>>= \case (Left e) -> return (Left e)
(Right r) -> Right <$> (print r
>> release conn)
除了我提到的改变之外,你还必须在最后粘贴一个 Right <$>
以从 IO ()
动作转换为 IO (Either Err ())
动作。 (稍后会详细介绍。)
好的,让我们尝试用上面的 ExceptT
绑定替换 IO
绑定。我将添加一个 '
来区分 ExceptT
版本和 IO
版本(例如 >>=' :: IO (Either Err a) -> (a -> IO (Either Err b)) -> IO (Either Err b)
)。
simpleVersion id c = connect c >>=' \conn -> (run . query id $ conn)
>>=' \r -> Right <$> (print r
>> {- IO >>! -} release conn)
这已经是一个改进,一些空格的更改使它变得更好。我还将包括一个 do
版本。
simpleVersion id c =
connect c >>=' \conn ->
(run . query id $ conn) >>=' \r ->
Right <$> (print r >> release conn)
simpleVersion id c = do
conn <- connect c
r <- run . query id $ conn
Right <$> (print r >> release conn)
对我来说,这看起来很干净!当然,在 main
中,您仍然需要 printErrorAndExit
,如:
main = do
v <- runExceptT (simpleVersion 0 defaultConfig)
either printErrorAndExit pure v
现在,关于 Right <$> (...)
...我说我想从 IO a
转换为 IO (Either Err a)
。好吧,这种事情就是 MonadTrans
class 存在的原因;让我们看看 ExceptT
:
的实现
instance MonadTrans (ExceptT e) where
lift = ExceptT . liftM Right
嗯,liftM
和(<$>)
是同一个函数,只是名字不同而已。所以如果我们忽略 newtype
包装和展开,我们得到
lift m = Right <$> m
!所以:
simpleVersion id c = do
conn <- connect c
r <- run . query id $ conn
lift (print r >> release conn)
如果您愿意,您也可以选择使用 liftIO
。不同之处在于 lift
总是通过一个转换器提升一个单子动作,但适用于任何一对包装类型和转换器类型;虽然 liftIO
通过 monad 转换器堆栈所需的尽可能多的转换器提升 IO
动作,但仅适用于 IO
动作。
当然,到目前为止我们已经省略了所有 newtype
包装和展开。为了使 simpleVersion
像我们这里的最后一个示例一样漂亮,您需要更改 connect
和 run
以适当地包含这些包装器。
我完全同意@Daniel Wagner 写的所有内容。但是还有另一种选择如何使用 IO (Either err a)
.
组织工作
有函数try :: Exception e => IO a -> IO (Either e a)
,我们可以实现它的反转版本(untry :: Exception e => IO (Either e a) -> IO a
)吗?让我们这样做:
untry :: Exception e => IO (Either e a) -> IO a
untry action = action >>= either throwIO pure
包 unliftio named fromEitherM 中有类似的功能。
现在我们可以重新设计您的示例:
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c = do
conn <- untry . connect $ c
r <- untry . run . query id $ conn
print r
release conn
为了使这个技巧起作用,错误类型需要是 Exception class.
的实例
printErrorAndExit
没有在这里使用,因为它应该在顶层完成(main
或接近它)。并且可以使用 catch
、try
.
等函数处理错误
唯一的问题是,如果在打开和关闭之间发生错误,则无法释放连接。让我们用 bracket
:
来解决这个问题
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c = bracket (untry $ connect c) release $ \conn -> do
r <- untry $ run $ query id conn
print r
您可以编写用于创建连接的辅助函数:
withConn :: Config -> (Connection -> IO a) -> IO a
withConn c = bracket (untry $ connect c) release
比:
simpleVersion id = flip withConn $ \conn -> do
r <- untry $ run $ query id conn
print r
你可以考虑使用 ReaderT Connection IO
来隐式提供与你的查询的连接。之后你的代码可以看起来像:
simpleVersion id conf = withConn conf $ do
r <- run $ query id
print r
我有一个连接到数据库然后运行查询的函数。这些步骤中的每一步都会导致 IO (Either SomeErrorType SomeResultType)
.
我非常喜欢在学习 Haskell 中使用 Either
和类似 monad 的其中一件事是能够使用像 >>=
这样的 monad 函数和像 mapLeft
以简化对预期错误状态的大量处理。
我在这里阅读博客文章、Control.Monad.Trans
文档和关于 SO 的其他答案的期望是,我必须以某种方式使用转换器/提升从 IO
上下文移动到 Either
上下文。
我的代码的一个更简单的例子:
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c =
connect c >>= \case
(Left e) -> printErrorAndExit e
(Right conn) -> (run . query id $ conn)
>>= \case
(Left e) -> printErrorAndExit e
(Right r) -> print r
>> release conn
我的问题是 (a) 我并不真正了解 ExceptT
如何让我到达与 mapLeft handleErrors $ eitherErrorOrResult >>= someOtherErrorOrResult >>= print
世界相似的地方的机制; (b) 我不确定如何确保始终以最好的方式释放连接(即使在我上面的简单示例中),尽管我想我会使用 bracket pattern.
我确定每个(相对)新的 Haskeller 都这么说,但我仍然真的不理解 monad 转换器,我读到的所有内容(除了前面链接的 SO 答案)对我来说太不透明了(还) .
如何将上面的代码转换成可以删除所有这些嵌套和错误处理的代码?
我认为查看 ExceptT
的 Monad
实例的源代码很有启发性:
newtype ExceptT e m a = ExceptT (m (Either e a))
instance (Monad m) => Monad (ExceptT e m) where
return a = ExceptT $ return (Right a)
m >>= k = ExceptT $ do
a <- runExceptT m
case a of
Left e -> return (Left e)
Right x -> runExceptT (k x)
如果忽略newtype
的包裹和展开,就更简单了:
m >>= k = do
a <- m
case a of
Left e -> return (Left e)
Right x -> k x
或者,您似乎不喜欢使用 do
:
m >>= k = m >>= \a -> case a of
Left e -> return (Left e)
Right x -> k x
你是不是觉得这段代码很眼熟?那和你的代码之间的唯一区别是你写 printErrorAndExit
而不是 return . Left
!所以,让我们把那个 printErrorAndExit
移到顶层,并且很高兴 记住 现在的错误而不打印它。
simpleVersion :: Integer -> Config -> IO (Either Err ())
simpleVersion id c = connect c >>= \case (Left e) -> return (Left e)
(Right conn) -> (run . query id $ conn)
>>= \case (Left e) -> return (Left e)
(Right r) -> Right <$> (print r
>> release conn)
除了我提到的改变之外,你还必须在最后粘贴一个 Right <$>
以从 IO ()
动作转换为 IO (Either Err ())
动作。 (稍后会详细介绍。)
好的,让我们尝试用上面的 ExceptT
绑定替换 IO
绑定。我将添加一个 '
来区分 ExceptT
版本和 IO
版本(例如 >>=' :: IO (Either Err a) -> (a -> IO (Either Err b)) -> IO (Either Err b)
)。
simpleVersion id c = connect c >>=' \conn -> (run . query id $ conn)
>>=' \r -> Right <$> (print r
>> {- IO >>! -} release conn)
这已经是一个改进,一些空格的更改使它变得更好。我还将包括一个 do
版本。
simpleVersion id c =
connect c >>=' \conn ->
(run . query id $ conn) >>=' \r ->
Right <$> (print r >> release conn)
simpleVersion id c = do
conn <- connect c
r <- run . query id $ conn
Right <$> (print r >> release conn)
对我来说,这看起来很干净!当然,在 main
中,您仍然需要 printErrorAndExit
,如:
main = do
v <- runExceptT (simpleVersion 0 defaultConfig)
either printErrorAndExit pure v
现在,关于 Right <$> (...)
...我说我想从 IO a
转换为 IO (Either Err a)
。好吧,这种事情就是 MonadTrans
class 存在的原因;让我们看看 ExceptT
:
instance MonadTrans (ExceptT e) where
lift = ExceptT . liftM Right
嗯,liftM
和(<$>)
是同一个函数,只是名字不同而已。所以如果我们忽略 newtype
包装和展开,我们得到
lift m = Right <$> m
!所以:
simpleVersion id c = do
conn <- connect c
r <- run . query id $ conn
lift (print r >> release conn)
如果您愿意,您也可以选择使用 liftIO
。不同之处在于 lift
总是通过一个转换器提升一个单子动作,但适用于任何一对包装类型和转换器类型;虽然 liftIO
通过 monad 转换器堆栈所需的尽可能多的转换器提升 IO
动作,但仅适用于 IO
动作。
当然,到目前为止我们已经省略了所有 newtype
包装和展开。为了使 simpleVersion
像我们这里的最后一个示例一样漂亮,您需要更改 connect
和 run
以适当地包含这些包装器。
我完全同意@Daniel Wagner 写的所有内容。但是还有另一种选择如何使用 IO (Either err a)
.
有函数try :: Exception e => IO a -> IO (Either e a)
,我们可以实现它的反转版本(untry :: Exception e => IO (Either e a) -> IO a
)吗?让我们这样做:
untry :: Exception e => IO (Either e a) -> IO a
untry action = action >>= either throwIO pure
包 unliftio named fromEitherM 中有类似的功能。
现在我们可以重新设计您的示例:
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c = do
conn <- untry . connect $ c
r <- untry . run . query id $ conn
print r
release conn
为了使这个技巧起作用,错误类型需要是 Exception class.
的实例printErrorAndExit
没有在这里使用,因为它应该在顶层完成(main
或接近它)。并且可以使用 catch
、try
.
唯一的问题是,如果在打开和关闭之间发生错误,则无法释放连接。让我们用 bracket
:
simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c = bracket (untry $ connect c) release $ \conn -> do
r <- untry $ run $ query id conn
print r
您可以编写用于创建连接的辅助函数:
withConn :: Config -> (Connection -> IO a) -> IO a
withConn c = bracket (untry $ connect c) release
比:
simpleVersion id = flip withConn $ \conn -> do
r <- untry $ run $ query id conn
print r
你可以考虑使用 ReaderT Connection IO
来隐式提供与你的查询的连接。之后你的代码可以看起来像:
simpleVersion id conf = withConn conf $ do
r <- run $ query id
print r