我想编写可能失败的操作,但有一种方法可以回滚。
例如 - 预订酒店房间的外部电话,以及向信用卡收费的外部电话。这两个电话都可能失败,例如没有房间,信用卡无效。两者都有办法回滚 - 取消酒店房间,取消信用费。
STM
。我觉得你可以编写一个monad Atomic T
来跟踪这些操作并在有异常的情况下将它们回滚。
编辑:
这些操作可能是IO
次操作。如果操作只是内存操作,正如两个答案所示,STM就足够了。
例如通过HTTP请求预订酒店。数据库操作,例如通过套接字通信插入记录。
在现实世界中,对于不可逆操作,在操作完成之前会有一段宽限期 - 例如信用卡付款和酒店预订可能会在当天结束时结算,因此可以在此之前取消。
答案 0 :(得分:6)
这正是STM的目的。组成动作以便它们自动地一起成功或失败。
非常类似于您的酒店房间问题,是Simon Peyton-Jones在“美丽代码”章节中的银行交易示例:http://research.microsoft.com/en-us/um/people/simonpj/papers/stm/beautiful.pdf
答案 1 :(得分:5)
如果您需要使用自己的monad,它将看起来像这样:
import Control.Exception (onException, throwIO)
newtype Rollbackable a = Rollbackable (IO (IO (), a))
runRollbackable :: Rollbackable a -> IO a
runRollbackable (Rollbackable m) = fmap snd m
-- you might want this to catch exceptions and return IO (Either SomeException a) instead
instance Monad Rollbackable where
return x = Rollbackable $ return (return (), x)
Rollbackable m >>= f
= do (rollback, x) <- m
Rollbackable (f x `onException` rollback)
(您可能也需要Functor
和Applicative
个实例,但它们很简单。)
您将以这种方式定义可回滚的原语操作:
rollbackableChargeCreditCard :: CardNumber -> CurrencyAmount -> Rollbackable CCTransactionRef
rollbackableChargeCreditCard ccno amount = Rollbackable
$ do ref <- ioChargeCreditCard ccno amount
return (ioUnchargeCreditCard ref, ref)
ioChargeCreditCard :: CardNumber -> CurrencyAmount -> IO CCTransactionRef
-- use throwIO on failure
ioUnchargeCreditCard :: CCTransactionRef -> IO ()
-- these both just do ordinary i/o
然后像这样运行它们:
runRollbackable
$ do price <- rollbackableReserveRoom roomRequirements when
paymentRef <- rollbackableChargeCreditCard ccno price
-- etc
答案 2 :(得分:1)
如果你的计算只能用TVar
类似的东西完成,那么STM
就是完美的。
如果您需要副作用(例如“为Bob充电100美元”)并且如果稍后发出错误(例如“退款Bob $ 100”)那么您需要,请鼓励:Control.Exceptions.bracketOnError
bracketOnError
:: IO a -- ^ computation to run first (\"acquire resource\")
-> (a -> IO b) -- ^ computation to run last (\"release resource\")
-> (a -> IO c) -- ^ computation to run in-between
-> IO c -- returns the value from the in-between computation
与
Control.Exception.bracket
类似,但只有在执行时才执行最终操作 中间计算引发的异常。
因此,我可以想象使用它:
let safe'charge'Bob = bracketOnError (charge'Bob) (\a -> refund'Bob)
safe'charge'Bob $ \a -> do
rest'of'transaction
which'may'throw'error
如果您使用的是多线程程序,请确保您了解Control.Exception.mask
操作的使用位置并尝试此类操作。
我应该强调,您可以而且应该阅读源代码Control.Exception和Control.Exception.Base,看看这是如何在GHC中完成的。
答案 3 :(得分:0)
你真的可以通过聪明的STM应用来做到这一点。关键是要分离IO部分。我假设问题是事务最初可能会成功,并且稍后才会失败。 (如果您可以立即识别失败,或者很快就会发现事情变得更简单):
main = do
r <- reserveHotel
c <- chargeCreditCard
let room = newTVar r
card = newTVar c
transFailure = newEmptyTMVar
rollback <- forkIO $ do
a <- atomically $ takeTMVar transFailure --blocks until we put something here
case a of
Left "No Room" -> allFullRollback
Right "Card declined" -> badCardRollback
failure <- listenForFailure -- A hypothetical IO action that blocks, waiting for
-- a failure message or an "all clear"
case failures of
"No Room" -> atomically $ putTMVar (Left "No Room")
"Card Declined" -> atomically $ putTMVar (Right "Card declined")
_ -> return ()
现在,MVars无法处理这里:我们所做的只是让线程等待,看看我们是否需要解决问题。但是你可能会在你的卡收费和酒店预订方面做其他一些事情......