Unlike dynamically dispatched effects, statically dispatched effects have a
single, set interpretation that cannot be changed at runtime, which makes
them useful in specific scenarios. For example:
If you'd like to ensure that a specific effect will behave in a certain way
at all times, using a statically dispatched version is the only way to
ensure that.
If the effect you're about to define has only one reasonable
implementation, it makes a lot of sense to make it statically dispatched.
Statically dispatched effects also perform slightly better than dynamically
dispatched ones, because their operations are implemented as standard top
level functions, so the compiler can apply more optimizations to them.
An example
Let's say that there exists a logging library whose functionality we'd like
to turn into an effect. Its Logger data type (after simplification) is
represented in the following way:
Because the Logger type itself allows customization of how messages are
logged, it is an excellent candidate to be turned into a statically
dispatched effect.
Such effect is represented by an empty data type of kind Effect:
Example1 expression
>>> data Log :: Effect
When it comes to the dispatch, we also need to signify whether core
operations of the effect will perform side effects. Since GHC is not a
polygraph, you can lie, though being truthful is recommended 🙂
Example1 expression
>>> type instance DispatchOf Log = Static WithSideEffects
The environment of Eff will hold the data type that represents the
effect. It is defined by the appropriate instance of the StaticRep data
family:
Example1 expression
>>> newtype instance StaticRep Log = Log Logger
Note: all operations of a statically dispatched effect will have a
read/write access to this data type as long as they can see its constructors,
hence it's best not to export them from the module that defines the effect.
That works, but has an unfortunate consequence: in order to use the log
operation the IOE effect needs to be in scope! This is bad, because we're
trying to limit (ideally, fully eliminate) the need to have the full power of
IO available in the application code. The solution is to use one of the
escape hatches that allow unrestricted access to the internal representation
of Eff:
Example1 expression
>>> :{ log :: Log :> es => String -> Eff es () log msg = do Log logger <- getStaticRep unsafeEff_ $ logMessage logger msg:}
However, since logging is most often an operation with side effects, in order
for this approach to be sound, the function that introduces the Log effect
needs to require the IOE effect.
If you forget to do that, don't worry. As long as the DispatchOf instance
was correctly defined to be StaticWithSideEffects, you will get a
reminder:
Example1 expression
>>> :{ runLog :: Logger -> Eff (Log : es) a -> Eff es a runLog logger = evalStaticRep (Log logger):}......No instance for ...IOE :> es... arising from a use of ‘evalStaticRep’...
Including IOE :> es in the context fixes the problem:
Example1 expression
>>> :{ runLog :: IOE :> es => Logger -> Eff (Log : es) a -> Eff es a runLog logger = evalStaticRep (Log logger):}
In general, whenever any operation of a statically dispatched effect performs
side effects using one of the unsafe functions, all functions that introduce
this effect need to require the IOE effect (otherwise it would be possible
to run it via runPureEff).
Now we can use the newly defined effect to log messages:
Signifies whether core operations of a statically dispatched effect perform
side effects. If an effect is marked as such, the
runStaticRep family of functions will require the
IOE effect to be in context via the
MaybeIOE type family.
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.