on master help sign in
More demonstrations of mtl issues
7a25ab9Andrzej Rybczak
effectful-core/src/Effectful/Error.hs+33−3
@@ -1,8 +1,6 @@
1-- | Support for handling errors of a particular type, i.e. checked exceptions.
1-- | Support for handling errors of a particular type, i.e. checked exceptions.
2--
2--
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
6-- exceptions, that's what functions from the @exceptions@ library are for (see
4-- exceptions, that's what functions from the @exceptions@ library are for (see
7-- "Control.Monad.Catch" for more information).
5-- "Control.Monad.Catch" for more information).
8--
6--
@@ -37,6 +35,38 @@
37-- Cleaning up.
35-- Cleaning up.
38-- Left "oops"
36-- Left "oops"
39--
37--
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.
40module Effectful.Error
70module Effectful.Error
41 ( Error
71 ( Error
42 , runError
72 , runError
effectful-core/src/Effectful/State/Shared.hs+24−1
@@ -3,8 +3,27 @@
3-- - shared between multiple threads (if you want each thead to manage its own
3-- - shared between multiple threads (if you want each thead to manage its own
4-- version of the value, see "Effectful.State.Local").
4-- version of the value, see "Effectful.State.Local").
5--
5--
6−-- - slower than "Effectful.State.Local".
6+-- - slightly slower than "Effectful.State.Local".
7--
7--
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!"
8module Effectful.State.Shared
27module Effectful.State.Shared
9 ( State
28 ( State
10 , runState
29 , runState
@@ -102,3 +121,7 @@ stateM f = unsafeEff $ \es -> do
102-- /Note:/ this function gets an exclusive access to the state for its duration.
121-- /Note:/ this function gets an exclusive access to the state for its duration.
103modifyM :: State s :> es => (s -> Eff es s) -> Eff es ()
122modifyM :: State s :> es => (s -> Eff es s) -> Eff es ()
104modifyM f = stateM (\s -> ((), ) <$> f s)
123modifyM 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
90 pure (a, f w)
90 pure (a, f w)
91
91
92-- $setup
92-- $setup
93−-- >>> import Control.Monad.Catch
94-- >>> import Control.Exception (ErrorCall)
93-- >>> import Control.Exception (ErrorCall)
94+-- >>> import Control.Monad.Catch
effectful-core/src/Effectful/Writer/Shared.hs+18−3
@@ -3,7 +3,7 @@
3-- - shared between multiple threads (if you want each thead to manage its own
3-- - shared between multiple threads (if you want each thead to manage its own
4-- version of the value, see "Effectful.Writer.Local").
4-- version of the value, see "Effectful.Writer.Local").
5--
5--
6−-- - slower than "Effectful.Writer.Local".
6+-- - slightly slower than "Effectful.Writer.Local".
7--
7--
8-- /Warning:/ 'Writer'\'s state will be accumulated via __left-associated__ uses
8-- /Warning:/ 'Writer'\'s state will be accumulated via __left-associated__ uses
9-- of '<>', which makes it unsuitable for use with types for which such pattern
9-- of '<>', which makes it unsuitable for use with types for which such pattern
@@ -25,7 +25,7 @@ module Effectful.Writer.Shared
25 ) where
25 ) where
26
26
27import Control.Concurrent.MVar
27import Control.Concurrent.MVar
28−import Control.Exception
28+import Control.Exception (onException, uninterruptibleMask)
29
29
30import Effectful.Dispatch.Static
30import Effectful.Dispatch.Static
31import Effectful.Monad
31import Effectful.Monad
@@ -61,7 +61,18 @@ tell w1 = unsafeEff $ \es -> do
61--
61--
62-- /Note:/ if a runtime exception is received while the action is executed, the
62-- /Note:/ if a runtime exception is received while the action is executed, the
63-- partial output of the action will still be appended to the overall output of
63-- 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!"
65listen :: (Writer w :> es, Monoid w) => Eff es a -> Eff es (a, w)
76listen :: (Writer w :> es, Monoid w) => Eff es a -> Eff es (a, w)
66listen m = unsafeEff $ \es -> do
77listen m = unsafeEff $ \es -> do
67 -- The mask is uninterruptible because modifyMVar_ v0 in the merge function
78 -- 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)
91listens f m = do
102listens f m = do
92 (a, w) <- listen m
103 (a, w) <- listen m
93 pure (a, f w)
104 pure (a, f w)
105+
106+-- $setup
107+-- >>> import Control.Exception (ErrorCall)
108+-- >>> import Control.Monad.Catch