HORIZON HASKELLDocslts/ghc-10.0.xfc4e5982026-10-11Search names, modules, packages, or :: a typeCtrl K

GHC 10.0.0.20260917 · lts/ghc-10.0.x · fc4e598 · 2026-10-11

Moduleeffectful-core-2.7.1.2GHC2021

Effectful.Dispatch.Dynamic

Dynamically dispatched effects.

  • 4 types
  • 2 classes
  • 33 values

Introduction

0 declarations

A dynamically dispatched effect is a collection of operations that can be interpreted in different ways at runtime, depending on the handler that is used to run the effect.

This allows a programmer to separate the what from the how, i.e. define effects that model what the code should do, while providing handlers that determine how it should do it later. Moreover, different environments can use different handlers to change the behavior of specific parts of the application if appropriate.

An example

Let's create an effect for basic file access, i.e. writing and reading files.

First, we need to define a generalized algebraic data type of kind Effect, where each constructor corresponds to a specific operation of the effect in question.

Example1 expression
:{  data FileSystem :: Effect where    ReadFile  :: FilePath -> FileSystem m String    WriteFile :: FilePath -> String -> FileSystem m ():}
Example1 expression
type instance DispatchOf FileSystem = Dynamic

The FileSystem effect has two operations:

  • ReadFile, which takes a FilePath and returns a String in the monadic context.

  • WriteFile, which takes a FilePath, a String and returns a () in the monadic context.

For people familiar with mtl style effects, note that the syntax looks very similar to defining an appropriate type class:

class FileSystem m where
  readFile  :: FilePath -> m String
  writeFile :: FilePath -> String -> m ()

The biggest difference between these two is that the definition of a type class gives us operations as functions, while the definition of an effect gives us operations as data constructors. They can be turned into functions with the help of send:

Example1 expression
:{  readFile :: (HasCallStack, FileSystem :> es) => FilePath -> Eff es String  readFile path = send (ReadFile path):}
Example1 expression
:{  writeFile :: (HasCallStack, FileSystem :> es) => FilePath -> String -> Eff es ()  writeFile path content = send (WriteFile path content):}

Note: the above functions and the DispatchOf instance can also be automatically generated by the makeEffect function from the effectful-th package.

The following defines an EffectHandler that reads and writes files from the drive:

Example3 expressions
import Effectful.Error.Staticimport Effectful.Exceptionimport System.IO qualified as IO
Example1 expression
newtype FsError = FsError String deriving Show
Example1 expression
:{ runFileSystemIO   :: (IOE :> es, Error FsError :> es)   => Eff (FileSystem : es) a   -> Eff es a runFileSystemIO = interpret $ \_ -> \case   ReadFile path           -> adapt $ IO.readFile path   WriteFile path contents -> adapt $ IO.writeFile path contents   where     adapt m = liftIO m `catchIO` \e -> throwError . FsError $ show e:}

Here, we use interpret and simply execute corresponding IO actions for each operation, additionally doing a bit of error management.

On the other hand, maybe there is a situation in which instead of interacting with the outside world, a pure, in-memory storage is preferred:

Example2 expressions
import Data.Map.Strict qualified as Mimport Effectful.State.Static.Local
Example1 expression
:{  runFileSystemPure    :: Error FsError :> es    => M.Map FilePath String    -> Eff (FileSystem : es) a    -> Eff es a  runFileSystemPure fs0 = reinterpret (evalState fs0) $ \_ -> \case    ReadFile path -> gets (M.lookup path) >>= \case      Just contents -> pure contents      Nothing       -> throwError . FsError $ "File not found: " ++ show path    WriteFile path contents -> modify $ M.insert path contents:}

Here, we use reinterpret and introduce a State effect for the storage that is private to the effect handler and cannot be accessed outside of it.

Let's compare how these differ.

Example1 expression
:{  action = do    file <- readFile "effectful-core.cabal"    pure $ length file > 0:}
Example1 expression
:t actionaction :: (FileSystem :> es) => Eff es Bool
Example1 expression
runEff . runError @FsError . runFileSystemIO $ actionRight True
Example1 expression
runPureEff . runErrorNoCallStack @FsError . runFileSystemPure M.empty $ actionLeft (FsError "File not found: \"effectful-core.cabal\"")

First order and higher order effects

Note that the definition of the FileSystem effect from the previous section doesn't use the m type parameter. What is more, when the effect is interpreted, the LocalEnv argument of the EffectHandler is also not used. Such effects are first order.

If an effect makes use of the m parameter, it is a higher order effect.

Note: for handling first order effects you can use interpret_ or reinterpret_ whose EffectHandler_ doesn't take the LocalEnv parameter.

Interpretation of higher order effects is slightly more involving. To see why, let's consider the Profiling effect for logging how much time a specific action took to run:

Example1 expression
:{  data Profiling :: Effect where    Profile :: String -> m a -> Profiling m a:}
Example1 expression
type instance DispatchOf Profiling = Dynamic
Example1 expression
:{  profile :: (HasCallStack, Profiling :> es) => String -> Eff es a -> Eff es a  profile label action = send (Profile label action):}

If we naively try to interpret it, we will run into trouble:

Example1 expression
import GHC.Clock (getMonotonicTime)
Example1 expression
:{ runProfiling :: IOE :> es => Eff (Profiling : es) a -> Eff es a runProfiling = interpret $ \_ -> \case   Profile label action -> do     t1 <- liftIO getMonotonicTime     r <- action     t2 <- liftIO getMonotonicTime     liftIO . putStrLn $ "Action '" ++ label ++ "' took " ++ show (t2 - t1) ++ " seconds."     pure r:}...... Couldn't match type ‘localEs’ with ‘es’...

The problem is that action has a type Eff localEs a, while the monad of the effect handler is Eff es. localEs represents the local environment in which the Profile operation was called, which is more or less opaque to the effect handler.

The solution is to use the LocalEnv that an EffectHandler is given to run the action using one of the functions from the localUnlift family:

Example1 expression
:{ runProfiling :: IOE :> es => Eff (Profiling : es) a -> Eff es a runProfiling = interpret $ \env -> \case   Profile label action -> localSeqUnliftIO env $ \unlift -> do     t1 <- getMonotonicTime     r <- unlift action     t2 <- getMonotonicTime     putStrLn $ "Action '" ++ label ++ "' took " ++ show (t2 - t1) ++ " seconds."     pure r:}

In a similar way we can define a dummy interpreter that does no profiling:

Example1 expression
:{ runNoProfiling :: Eff (Profiling : es) a -> Eff es a runNoProfiling = interpret $ \env -> \case   Profile label action -> localSeqUnlift env $ \unlift -> unlift action:}

...and it's done.

Example1 expression
action = profile "greet" . liftIO $ putStrLn "Hello!"
Example1 expression
:t actionaction :: (Profiling :> es, IOE :> es) => Eff es ()
Example1 expression
runEff . runProfiling $ actionHello!Action 'greet' took ... seconds.
Example1 expression
runEff . runNoProfiling $ actionHello!

Integration with mtl style effects

There exists a lot of libraries that provide their functionality as an mtl style effect, which generally speaking is a type class that contains core operations of the library in question.

Such effects are quite easy to use with the Eff monad. As an example, consider the mtl style effect for generation of random numbers:

Example1 expression
:{  class Monad m => MonadRNG m where    randomInt :: m Int:}

Let's say the library also defines a helper function for generation of random strings:

Example2 expressions
import Control.Monadimport Data.Char
Example1 expression
:{ randomString :: MonadRNG m => Int -> m String randomString n = map chr <$> replicateM n randomInt:}

To make it possible to use it with the Eff monad, the first step is to create an effect with operations that mirror the ones of a type class:

Example1 expression
:{  data RNG :: Effect where    RandomInt :: RNG m Int:}
Example1 expression
type instance DispatchOf RNG = Dynamic

If we continued as in the example above, we'd now create top level helper functions that execute effect operations using send, in this case randomInt tied to RandomInt. But this function is already declared by the MonadRNG type class! Therefore, what we do instead is provide an orphan, canonical instance of MonadRNG for Eff that delegates to the RNG effect:

Example1 expression
:{  instance RNG :> es => MonadRNG (Eff es) where    randomInt = send RandomInt:}

Now we only need an interpreter:

Example1 expression
:{  runDummyRNG :: Eff (RNG : es) a -> Eff es a  runDummyRNG = interpret_ $ \case    RandomInt -> pure 55:}

and we can use any function that requires a MonadRNG constraint with the Eff monad as long as the RNG effect is in place:

Example1 expression
runEff . runDummyRNG $ randomString 3"777"

Functional dependencies

For dealing with classes that employ functional dependencies an additional trick is needed.

Consider the following:

Example1 expression
:set -XFunctionalDependencies
Example1 expression
:{  class Monad m => MonadInput i m | m -> i where    input :: m i:}

An attempt to define the instance as in the example above leads to violation of the liberal coverage condition:

Example1 expression
:{  instance Reader i :> es => MonadInput i (Eff es) where    input = ask:}......Illegal instance declaration for ‘MonadInput i (Eff es)’......  The liberal coverage condition fails in class ‘MonadInput’......    for functional dependency: ‘m -> i’......

However, there exists a trick for bypassing the coverage condition, i.e. including the instance head in its context:

Example1 expression
:{  instance (MonadInput i (Eff es), Reader i :> es) => MonadInput i (Eff es) where    input = ask:}

Now the MonadInput class can be used with the Eff monad:

Example1 expression
:{  double :: MonadInput Int m => m Int  double = (+) <$> input <*> input:}
Example1 expression
runPureEff . runReader @Int 3 $ double6

Sending operations to the handler

2 declarations

Handling effects

9 declarations
valuereinterpret
  1. :: (HasCallStack, DispatchOf e ~ 'Dynamic)
  2. => (Eff handlerEs a -> Eff es b)

    Setup of effects encapsulated in the handler.

  3. -> EffectHandler e handlerEs

    The effect handler.

  4. -> Eff (e ': es) a
  5. -> Eff es b
#

Interpret an effect using other, private effects.

interpret ≡ reinterpret id

Note: If you want to interpret multiple effects using other, private effects, you can do so with a combination of interpret and inject.

This is in particular useful for splitting a large effect into smaller ones. For example, let's say you want to split a State into a read only and read write component:

Example1 expression
:{ data Get s :: Effect where   Get :: Get s m s type instance DispatchOf (Get s) = Dynamic:}
Example1 expression
:{ data Put s :: Effect where   Put :: s -> Put s m () type instance DispatchOf (Put s) = Dynamic:}
Example1 expression
import Effectful.State.Static.Local qualified as S
Example1 expression
:{ runGetPut :: forall s es a. s -> Eff (Get s : Put s : es) a -> Eff es (a, s) runGetPut s0   = S.runState s0   . interpret_ @(Put s) (\(Put s) -> S.put s)   . interpret_ @(Get s) (\Get -> S.get)   . inject:}

Here, a State effect is introduced, then Put and Get effects that use it underneath and finally inject hides the original state from downstream code.

valueinterpose
  1. :: (HasCallStack, DispatchOf e ~ 'Dynamic, e :> es)
  2. => EffectHandler e es

    The effect handler.

  3. -> Eff es a
  4. -> Eff es a
#

Replace the handler of an existing effect with a new one.

Note: this function allows for augmenting handlers with a new functionality as the new handler can send operations to the old one.

Example1 expression
:{  data E :: Effect where    Op1 :: E m ()    Op2 :: E m ()    Op3 :: E m ()  type instance DispatchOf E = Dynamic:}
Example1 expression
:{  runE :: IOE :> es => Eff (E : es) a -> Eff es a  runE = interpret_ $ \case    Op1 -> liftIO (putStrLn "op1")    Op2 -> liftIO (putStrLn "op2")    Op3 -> error "Op3 not implemented":}
Example1 expression
let action = send Op1 >> send Op2
Example1 expression
runEff . runE $ actionop1op2

You can modify only specific operations and send the rest to the upstream handler with passthrough:

Example1 expression
:{  augmentOp2 :: (E :> es, IOE :> es) => Eff es a -> Eff es a  augmentOp2 = interpose $ \env -> \case    Op2 -> liftIO (putStrLn "augmented op2") >> send Op2    op  -> passthrough env op:}
Example1 expression
runEff . runE . augmentOp2 $ actionop1augmented op2op2

Note: when an exception is raised while handling an operation, good debugging experience is ensured by strategically placed HasCallStack constraints:

Example1 expression
runEff . runE . augmentOp2 $ send Op3*** Exception: Op3 not implemented...  error, called at <interactive>:...  handler, called at src/Effectful/Dispatch/Dynamic.hs:...  passthrough, called at <interactive>:...  handler, called at src/Effectful/Dispatch/Dynamic.hs:...  send, called at <interactive>:......

Handling local Eff computations

newtypenewtype LocalEnv (localEs :: [Effect])
#

Opaque representation of the Eff environment at the point of calling the send function, i.e. right before the control is passed to the effect handler.

Note: functions that consume it perform runtime checks to ensure that it's used within the scope of the effect handler it belongs to.

Unlifts

Lifts

valuewithLiftMap
  1. :: HasCallStack
  2. => LocalEnv localEs

    Local environment.

  3. -> ((forall a b. (Eff es a -> Eff es b) -> Eff localEs a -> Eff localEs b) -> Eff es r)

    Continuation with the lifting function in scope.

  4. -> Eff es r
#

Deprecated. Misusing withLiftMap in multiple threads results in undefined behavior. Use localLiftUnlift with an appropriate UnliftStrategy instead.

Utility for lifting Eff computations of type

Eff es a -> Eff es b

to

Eff localEs a -> Eff localEs b

Note: the computation must not run its argument in a different thread, attempting to do so will result in a runtime error.

Warning: if the lifting function is used in a thread distinct from its creator, the lifted computation must not interact with the environment. This cannot be detected at runtime, hence the deprecation.

valuewithLiftMapIO
  1. :: (HasCallStack, IOE :> es)
  2. => LocalEnv localEs

    Local environment.

  3. -> ((forall a b. (IO a -> IO b) -> Eff localEs a -> Eff localEs b) -> Eff es r)

    Continuation with the lifting function in scope.

  4. -> Eff es r
#

Utility for lifting IO computations of type

IO a -> IO b

to

Eff localEs a -> Eff localEs b

Note: the computation must not run its argument in a different thread, attempting to do so will result in a runtime error.

Useful e.g. for lifting the unmasking function in mask-like computations:

Example1 expression
:{data Fork :: Effect where  ForkWithUnmask :: ((forall a. m a -> m a) -> m ()) -> Fork m ThreadIdtype instance DispatchOf Fork = Dynamic:}
Example1 expression
:{runFork :: IOE :> es => Eff (Fork : es) a -> Eff es arunFork = interpret $ \env (ForkWithUnmask m) -> withLiftMapIO env $ \liftMap -> do  localUnliftIO env (ConcUnlift Ephemeral $ Limited 1) $ \unlift -> do    forkIOWithUnmask $ \unmask -> unlift $ m $ liftMap unmask:}

Bidirectional lifts

valuelocalLiftUnlift
  1. :: HasCallStack
  2. => LocalEnv localEs

    Local environment.

  3. -> UnliftStrategy
  4. -> ((forall r. Eff es r -> Eff localEs r) -> (forall r. Eff localEs r -> Eff es r) -> Eff es a)

    Continuation with the lifting and unlifting function in scope.

  5. -> Eff es a
#

Create a local lifting and unlifting function with the given strategy.

Useful for lifting complicated Eff computations where the monadic action shows in both positive (as a result) and negative (as an argument) position.

Note: when SeqForkUnlift or ConcUnlift Persistent strategy is used, the unlifting functions will share the effect storage (unlike with two separate calls to localLift and localUnlift).

valuelocalLiftUnliftIO
  1. :: (HasCallStack, IOE :> es)
  2. => LocalEnv localEs

    Local environment.

  3. -> UnliftStrategy
  4. -> ((forall r. IO r -> Eff localEs r) -> (forall r. Eff localEs r -> IO r) -> IO a)

    Continuation with the lifting and unlifting function in scope.

  5. -> Eff es a
#

Create a local unlifting function with the given strategy along with an unrestricted lifting function.

Useful for lifting complicated IO computations where the monadic action shows in both positive (as a result) and negative (as an argument) position.

Note: depending on the computation you're lifting localUnliftIO along with withLiftMapIO might be enough and is more efficient.

Misc

valuelocalSeqLend
  1. :: (HasCallStack, KnownSubset lentEs es)
  2. => LocalEnv localEs
  3. -> ((forall r. Eff (lentEs ++ localEs) r -> Eff localEs r) -> Eff es a)

    Continuation with the lent handler in scope.

  4. -> Eff es a
#

Lend effects to the local environment.

Consider the following effect:

Example1 expression
:{  data D :: Effect where    D :: D m ()  type instance DispatchOf D = Dynamic:}

and an auxiliary effect that requires both IOE and D to run:

Example1 expression
:{  data E :: Effect  runE :: (IOE :> es, D :> es) => Eff (E : es) a -> Eff es a  runE = error "runE":}

Trying to use runE inside the handler of D doesn't work out of the box:

Example1 expression
:{  runD :: IOE :> es => Eff (D : es) a -> Eff es a  runD = interpret $ \env -> \case    D -> localSeqUnlift env $ \unlift -> do      unlift . runE $ pure ():}......Could not deduce ...IOE :> localEs... arising from a use of ‘runE’...from the context: IOE :> es...

The problem is that runE needs IOE :> localEs, but only IOE :> es is available. This function allows us to bridge the gap:

Example1 expression
:{  runD :: IOE :> es => Eff (D : es) a -> Eff es a  runD = interpret $ \env -> \case    D -> localSeqUnlift env $ \unlift -> do      localSeqLend @'[IOE] env $ \useIOE -> do        unlift . useIOE . runE $ pure ():}
valuelocalLendBorrow
  1. :: (HasCallStack, KnownSubset lentEs es, KnownSubset borrowedEs localEs)
  2. => LocalEnv localEs
  3. -> UnliftStrategy
  4. -> ((forall r. Eff (lentEs ++ localEs) r -> Eff localEs r) -> (forall r. Eff (borrowedEs ++ es) r -> Eff es r) -> Eff es a)

    Continuation with the lending and borrowing functions in scope.

  5. -> Eff es a
#

Simultaneously lend effects to the local environment and borrow effects from it with a given unlifting strategy.

Note: when SeqForkUnlift or ConcUnlift Persistent strategy is used, the lending and borrowing functions will share the effect storage (unlike with two separate calls to localLend and localBorrow).

classclass SharedSuffix (es1 :: [Effect]) (es2 :: [Effect])
#

Deprecated. Runtime sanity checks in relevant functions make this constraint unnecessary.

Require that both effect stacks share an opaque suffix.

Functions from the localUnlift family previously required this constraint to reject a subset of improper uses of unlifting functions at compile time. It's no longer necessary, since all of them are detected at runtime now.

As an example, consider the following higher order effect:

Example1 expression
:{  data E :: Effect where    E :: m a -> E m a  type instance DispatchOf E = Dynamic:}

Running local actions in a more specific environment is fine:

Example1 expression
:{ runE1 :: Eff (E : es) a -> Eff es a runE1 = interpret $ \env -> \case   E m -> runReader () $ do     localSeqUnlift env $ \unlift -> unlift m:}

Running local actions in a more general environment is fine:

Example1 expression
:{ runE2 :: Eff (E : es) a -> Eff es a runE2 = reinterpret (runReader ()) $ \env -> \case   E m -> raise $ do     localSeqUnlift env $ \unlift -> unlift m:}

However, running local actions in an unrelated environment is not fine as this would make it possible to run anything within runPureEff:

Example1 expression
:{ runE3 :: Eff (E : es) a -> Eff es a runE3 = reinterpret (runReader ()) $ \env -> \case   E m -> pure . runPureEff $ do     localSeqUnlift env $ \unlift -> unlift m:}
Example1 expression
runEff . runE3 $ send (E (pure 'x'))*** Exception: Env and LocalEnv point to different Storages....
Instances4SharedSuffix
  • SharedSuffix es esDefined in effectful-core-2.7.1.2 · Effectful.Dispatch.Dynamic
  • SharedSuffix es1 es2 => SharedSuffix es1 (e ': es2)Defined in effectful-core-2.7.1.2 · Effectful.Dispatch.Dynamic
  • TypeError ('Text "Running local actions in monomorphic effect stacks is not supported." ':$$: 'Text "As a solution simply change the stack to have a polymorphic suffix.") => SharedSuffix '[] '[]Defined in effectful-core-2.7.1.2 · Effectful.Dispatch.Dynamic

    This is always preferred to SharedSuffix es es as it's not incoherent.

  • SharedSuffix es1 es2 => SharedSuffix (e ': es1) es2Defined in effectful-core-2.7.1.2 · Effectful.Dispatch.Dynamic
classclass Subset subEs es => KnownSubset (subEs :: [Effect]) (es :: [Effect])
#

Provide evidence that subEs is a known subset of es.

Instances2KnownSubset
  • KnownSubset '[] esDefined in effectful-core-2.7.1.2 · Effectful.Internal.Effect
  • (e :> es, KnownSubset subEs es) => KnownSubset (e ': subEs) esDefined in effectful-core-2.7.1.2 · Effectful.Internal.Effect
familytype family (++) (xs :: [Effect]) (ys :: [Effect]) :: [Effect] where
#

Append two type-level lists together.

Equations

  • (++) '[] ys = ys
  • (++) (x ': xs) ys = x ': xs ++ ys

Utils for first order effects

Re-exports

1 declaration
typetype HasCallStack = IP "callStack" CallStack
#

Request a CallStack.

NOTE: The implicit parameter ?callStack :: CallStack is an implementation detail and should not be considered part of the CallStack API, we may decide to change the implementation in the future.