on master help sign in
More demonstrations of mtl issues
commit7a25ab972f1037eaebc5c9f1c80399f5fa3623bf authorAndrzej Rybczak <andrzej@rybczak.net> dateFri 24 Dec 2021, 07:14 (5 years ago) parentbf25f34
4 files changed+76−8
effectful-core/src/Effectful/Error.hs+33−3 effectful-core/src/Effectful/State/Shared.hs+24−1 effectful-core/src/Effectful/Writer/Local.hs+1−1 effectful-core/src/Effectful/Writer/Shared.hs+18−3
effectful-core/src/Effectful/Error.hs+33−3
@@ -1,8 +1,6 @@
11-- | Support for handling errors of a particular type, i.e. checked exceptions.
22--
3−-- /Note:/ the 'Error' effect is a suitable replacement for the
4−-- 'Control.Monad.Trans.Except.ExceptT' monad transformer from the
5−-- @transformers@ library. It is __not__ a general mechanism for handling
3+-- The 'Error' effect is __not__ a general mechanism for handling regular
64-- exceptions, that's what functions from the @exceptions@ library are for (see
75-- "Control.Monad.Catch" for more information).
86--
@@ -37,6 +35,38 @@
3735-- Cleaning up.
3836-- Left "oops"
3937--
38+-- /Note:/ unlike the 'Control.Monad.Trans.Except.ExceptT' monad transformer
39+-- from the @transformers@ library, the order in which you handle the 'Error'
40+-- effect with regard to other stateful effects does not matter. Consider the
41+-- following:
42+--
43+-- >>> import qualified Control.Monad.State.Strict as T
44+-- >>> import qualified Control.Monad.Except as T
45+--
46+-- >>> let m1 = (T.modify (++ " there!") >> T.throwError "oops") `T.catchError` \_ -> pure ()
47+--
48+-- >>> (`T.runStateT` "Hi") . T.runExceptT $ m1
49+-- (Right (),"Hi there!")
50+--
51+-- >>> T.runExceptT . (`T.runStateT` "Hi") $ m1
52+-- Right ((),"Hi")
53+--
54+-- Here, whether state modifications within the @catchError@ block are lost or
55+-- not depends on the shape of the monad transformer stack, which is surprising
56+-- and can be a source of subtle bugs. On the other hand:
57+--
58+-- >>> import Effectful.State.Local
59+--
60+-- >>> let m2 = (modify (++ " there!") >> throwError "oops") `catchError` \_ (_::String) -> pure ()
61+--
62+-- >>> runEff . runState "Hi" . runError @String $ m2
63+-- (Right (),"Hi there!")
64+--
65+-- >>> runEff . runError @String . runState "Hi" $ m2
66+-- Right ((),"Hi there!")
67+--
68+-- Here, no matter the order of effects, state modifications within the
69+-- @catchError@ block always persist, giving predictable behavior.
4070module Effectful.Error
4171 ( Error
4272 , runError
effectful-core/src/Effectful/State/Shared.hs+24−1
@@ -3,8 +3,27 @@
33-- - shared between multiple threads (if you want each thead to manage its own
44-- version of the value, see "Effectful.State.Local").
55--
6−-- - slower than "Effectful.State.Local".
6+-- - slightly slower than "Effectful.State.Local".
77--
8+-- /Note:/ unlike the 'Control.Monad.Trans.State.StateT' monad transformer from
9+-- the @transformers@ library, the 'State' effect doesn't lose state
10+-- modifications when an exception is received:
11+--
12+-- >>> import qualified Control.Monad.Trans.State.Strict as S
13+--
14+-- >>> :{
15+-- (`S.execStateT` "Hi") . handle (\(_::ErrorCall) -> pure ()) $ do
16+-- S.modify (++ " there!")
17+-- error "oops"
18+-- :}
19+-- "Hi"
20+--
21+-- >>> :{
22+-- runEff . execState "Hi" . handle (\(_::ErrorCall) -> pure ()) $ do
23+-- modify (++ " there!")
24+-- error "oops"
25+-- :}
26+-- "Hi there!"
827module Effectful.State.Shared
928 ( State
1029 , runState
@@ -102,3 +121,7 @@ stateM f = unsafeEff $ \es -> do
102121-- /Note:/ this function gets an exclusive access to the state for its duration.
103122modifyM :: State s :> es => (s -> Eff es s) -> Eff es ()
104123modifyM f = stateM (\s -> ((), ) <$> f s)
124+
125+-- $setup
126+-- >>> import Control.Exception (ErrorCall)
127+-- >>> import Control.Monad.Catch
effectful-core/src/Effectful/Writer/Local.hs+1−1
@@ -90,5 +90,5 @@ listens f m = do
9090 pure (a, f w)
9191
9292-- $setup
93−-- >>> import Control.Monad.Catch
9493-- >>> import Control.Exception (ErrorCall)
94+-- >>> import Control.Monad.Catch
effectful-core/src/Effectful/Writer/Shared.hs+18−3
@@ -3,7 +3,7 @@
33-- - shared between multiple threads (if you want each thead to manage its own
44-- version of the value, see "Effectful.Writer.Local").
55--
6−-- - slower than "Effectful.Writer.Local".
6+-- - slightly slower than "Effectful.Writer.Local".
77--
88-- /Warning:/ 'Writer'\'s state will be accumulated via __left-associated__ uses
99-- of '<>', which makes it unsuitable for use with types for which such pattern
@@ -25,7 +25,7 @@ module Effectful.Writer.Shared
2525 ) where
2626
2727import Control.Concurrent.MVar
28−import Control.Exception
28+import Control.Exception (onException, uninterruptibleMask)
2929
3030import Effectful.Dispatch.Static
3131import Effectful.Monad
@@ -61,7 +61,18 @@ tell w1 = unsafeEff $ \es -> do
6161--
6262-- /Note:/ if a runtime exception is received while the action is executed, the
6363-- partial output of the action will still be appended to the overall output of
64−-- the 'Writer'.
64+-- the 'Writer':
65+--
66+-- >>> :{
67+-- runEff . execWriter @String $ do
68+-- tell "Hi"
69+-- handle (\(_::ErrorCall) -> pure ((), "")) $ do
70+-- tell " there"
71+-- listen $ do
72+-- tell "!"
73+-- error "oops"
74+-- :}
75+-- "Hi there!"
6576listen :: (Writer w :> es, Monoid w) => Eff es a -> Eff es (a, w)
6677listen m = unsafeEff $ \es -> do
6778 -- The mask is uninterruptible because modifyMVar_ v0 in the merge function
@@ -91,3 +102,7 @@ listens :: (Writer w :> es, Monoid w) => (w -> b) -> Eff es a -> Eff es (a, b)
91102listens f m = do
92103 (a, w) <- listen m
93104 pure (a, f w)
105+
106+-- $setup
107+-- >>> import Control.Exception (ErrorCall)
108+-- >>> import Control.Monad.Catch