module Effectful.Concurrent.Effect ( -- * Effect Concurrent -- ** Handlers , runConcurrent ) where import Effectful import Effectful.Dispatch.Static -- | Provide the ability to run 'Eff' computations concurrently in multiple -- threads and communicate between them. -- -- /Warning:/ unless you stick to high level functions from the -- 'Effectful.Concurrent.Async.withAsync' family, the 'Concurrent' effect makes -- it possible to escape the scope of any scoped effect operation. Consider the -- following: -- -- >>> import Effectful.Reader.Static qualified as R -- -- >>> printAsk msg = liftIO . putStrLn . (msg ++) . (": " ++) =<< R.ask -- -- >>> :{ -- runEff . R.runReader "GLOBAL" . runConcurrent $ do -- a <- R.local (const "LOCAL") $ do -- a <- async $ do -- printAsk "child (first)" -- threadDelay 20000 -- printAsk "child (second)" -- threadDelay 10000 -- printAsk "parent (inside)" -- pure a -- printAsk "parent (outside)" -- wait a -- :} -- child (first): LOCAL -- parent (inside): LOCAL -- parent (outside): GLOBAL -- child (second): LOCAL -- -- Note that the asynchronous computation doesn't respect the scope of -- 'Effectful.Reader.Static.local', i.e. the child thread still behaves like -- it's inside the 'Effectful.Reader.Static.local' block, even though the parent -- thread already got out of it. -- -- This is because the value provided by the t'Effectful.Reader.Static.Reader' -- effect is thread local, i.e. each thread manages its own version of it. For -- the t'Effectful.Reader.Static.Reader' it is the only reasonable behavior, it -- wouldn't be very useful if its "read only" value was affected by calls to -- 'Effectful.Reader.Static.local' from its parent or child threads. -- -- However, the cut isn't so clear if it comes to effects that provide access to -- a mutable state. That's why statically dispatched @State@ and @Writer@ -- effects come in two flavors, local and shared: -- -- >>> import Effectful.State.Static.Local qualified as SL -- >>> :{ -- runEff . SL.execState "Hi" . runConcurrent $ do -- replicateConcurrently_ 3 $ SL.modify (++ "!") -- :} -- "Hi" -- -- >>> import Effectful.State.Static.Shared qualified as SS -- >>> :{ -- runEff . SS.execState "Hi" . runConcurrent $ do -- replicateConcurrently_ 3 $ SS.modify (++ "!") -- :} -- "Hi!!!" -- -- In the first example state updates made concurrently are not reflected in the -- parent thread because the value is thread local, but in the second example -- they are, because the value is shared. -- data Concurrent :: Effect type instance DispatchOf Concurrent = Static WithSideEffects data instance StaticRep Concurrent = Concurrent -- | Run the 'Concurrent' effect. runConcurrent :: (HasCallStack, IOE :> es) => Eff (Concurrent : es) a -> Eff es a runConcurrent :: forall (es :: [Effect]) a. (HasCallStack, IOE :> es) => Eff (Concurrent : es) a -> Eff es a runConcurrent = StaticRep Concurrent -> Eff (Concurrent : es) a -> Eff es a forall (e :: Effect) (sideEffects :: SideEffects) (es :: [Effect]) a. (HasCallStack, DispatchOf e ~ 'Static sideEffects, MaybeIOE sideEffects es) => StaticRep e -> Eff (e : es) a -> Eff es a evalStaticRep StaticRep Concurrent Concurrent -- $setup -- >>> import Effectful.Concurrent -- >>> import Effectful.Concurrent.Async