ExtensibleEffects Public API

Usage and Syntax


@syntax_eff begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

Extensible Effects syntax. It applies effect to all values which are not prefaced with @pure, which lifts them into the Eff monad. The results are combined using @syntax_flatmap on the respective Eff monad, which finally is executed using autorun algorithm.


autohandled_eff = @syntax_eff begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

mycustomwrapper(i::Int) = collect(1:i)
# monadic-like syntax to first apply a wrapper before interpreting code to unhandled effects
autohandled_eff = @syntax_eff mycustomwrapper begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

The Interface

If you want to add support for new types, you need to provide the following interface: (Vector is only an example)

core functiondescription
ExtensibleEffects.eff_applies(handler::Type{<:Vector}, effectful::Vector) = truespecify on which values the handler applies (the handler Vector applies to Vector of course)
ExtensibleEffects.eff_pure(handler::Type{<:Vector}, value) = [value]wrap a plain value into the Monad of the handler, here Vector.
ExtensibleEffects.eff_flatmap(continuation, effectful::Vector)apply a continuation to the current effect (here again Vector as an example). The key difference to plain TypeClasses.flatmap is that continuation does not return a plain Vector, but a Eff{Vector}. Applying this continuation with a plain map would lead Vector{Eff{Vector}}. However, eff_flatmap needs to return an Eff{Vector} instead.
@syntax_eff_noautorun begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

Extensible Effects syntax which does not run autorun routine. As @syntax_eff, it applies effect to all values which are not prefaced with @pure, which lifts them into the Eff monad. The results are combined using @syntax_flatmap on the respective Eff monad, which then is directly returned.


unhandled_eff = @syntax_eff_noautorun begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

mycustomwrapper(i::Int) = collect(1:i)
# monadic-like syntax to first apply a wrapper before interpreting code to unhandled effects
unhandled_eff = @syntax_eff_noautorun mycustomwrapper begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b

Constructs a wrapper function which can be used within @syntax_eff to skip all given handler types within the autorun.


@syntax_eff noautorun(Vector, Identity) begin
  a = [1,2,3]
  b = Identity(a + 2)

will actually run no handler at all in the implicit autorun, as both handlers are marked for ignore.

Explicit introduction of effects

Explicit use of handlers

runhandlers(handlers, eff)
runhandlers((Vector, Option), eff)::Vector{Option{...}}

run all handlers such that the first handler will define the most outer container

@runhandlers handlers eff

For convenience we provide runhandlers function also as a macro.

With this you can easier run left-over handlers from an @syntax_eff autorun.


@runhandlers WithCall(args, kwargs) @syntax_eff begin
  a = Callable(x -> 2x)
  @pure a


Core Interface

ExtensibleEffects.eff_applies(handler::YourHandlerType, value::ToBeHandledEffectType) = true

Overwrite this function like above to indicate that a concrete effect is handled by a handler. In most cases you will have YourHandlerType = Type{ToBeHandledEffectType}, like for Vector or similar.

Sometimes you need extra information without which you cannot run a specific effect. Then you need to link the specific handler containing the required information. E.g. Callable needs args and kwargs to be run, which are captured in the handler type CallableHandler(args, kwargs). Hence above you would choose YourHandlerType = CallableHandler, and ToBeHandledEffectType = Callable.

ExtensibleEffects.eff_pure(handler, value)

Overwrite this for your custom effect handler, return either ExtensibleEffects.Eff type, or a plain value. Plain values will be wrapped with noeffect automatically.

ExtensibleEffects.eff_flatmap(handler, interpreted_continuation, value)
ExtensibleEffects.eff_flatmap(interpreted_continuation, value)

Overwrite this for your custom effect handler to handle your effect. This function is only called if eff_applies(handler, value)==true.

While for custom effects it is handy to dispatch on the handler itself, in simple cases handler == typeof(value) and hence, we allow to ommit it.


The arg interpreted_continuation is guaranteed to return an Eff of the handled type. E.g. if you might handle the type Vector, you are guaranteed that interpreted_continuation(x)::ExtensibleEffects.Eff{Vector}


If you do not return an ExtensibleEffects.Eff, the result will be wrapped into noeffect automatically, i.e. assuming the effect is handled afterwards.

optional extra for autorun (in 99% not needed)

eff_autohandler(value) = Base.typename(typeof(value)).wrapper

Overwrite this if the default autohandler extraction does not work for your case. E.g. for value::Vector the default would return Array, hence we need to overwrite it individually.

for developing advanced effects like State

runhandler(handler, eff::Eff)
runhandler(handler, eff::Eff, context)

key method to run an effect on some effecthandler Eff

note that we represent effectrunners as plain types in order to associate standard effect runners with types like Vector, Option, ...

for developing composable effect-handler-macros

@insert_into_runhandlers outer_handler @syntax_eff begin
  # ...

Next to simple Effects which can be directly composed down to plain values and reconstructed again from plain values, there are also a couple of more elaborate Effects, which values cannot be extracted without providing further context.

Callables are a good example. It is impossible to extract the values of a callable without calling it, or without constructing another Callable around it.

With all these example the typical flow would be like

Callable(function (args...; kwargs...)
  callablehandler = CallableHandler(args...; kwargs...)

  SomeOtherNeededContext() do info
    otherhandler = SomeOtherHandler(info)

    # ... possibly further nestings

    @runhandlers (callablehandler, otherhandler #= possible further handlers =#) @syntax_eff begin
      # ...

@inser_into_runhandlers can be used to simplify and even separate these outer handlers from one another, so that they can be used as composable interchangeable macros.

For example, here the implementation of @runcallable (ignoring macro hygiene)

macro runcallable(expr)
  :(Callable(function(args...; kwargs...)
    @insert_into_runhandlers(CallableHandler(args...; kwargs...), $expr)

This will search for an existing call to runhandlers within the given expr, and if found, inserts the CallableHandler similar to the motivating example above. If no runhandlers is found, it will create a new one.

This way, you can compose the nested code-structures very easily. You only have to be careful, that you always run all outer effects at once, in one single statement, so that @insert_into_runhandlers can actually find the right runhandlers.

Effect Handlers



Handler for generic Writers. The default accumulator works with Option values.


CallableHandler(args...; kwargs...)

Handler for functions, providing the arguments and keyword arguments for calling the functions.


translates to

Callable(function(args...; kwargs...)
  @insert_into_runhandlers CallableHandler(args...; kwargs...) eff

Thanks to @insert_into_runhandlers this outer runner can compose well with other outer runners.


@runstate eff

Note that unlike Callable, a State has to ensure that it is always the first outer Eff being run, as it returns the inner state as an additional argument.

If you would nest it with runcallable, e.g. like @runstate @runcallable eff it wouldn't work, as now the appended state is within the Callable and not directly within the State.



Handler for DataTypesBasic.ContextManager.

The naive handler implementation for contextmanager would immediately run the continuation within the contextmanager. However this does not work, as handling one effect does not mean that all "inner" effects are already handled. Hence, such a handler would actually initialize and finalize the contextmanager, without its value being processed already. When the other "inner" effects are run later on, they would find an already destroyed contextmanager session. We need to make sure, that the contextmanager is really the last Effect run. Therefore we create a custom handler.


translates to

  @insert_into_runhandlers ContextManagerHandler(cont) eff

Thanks to @insert_into_runhandlers this outer runner can compose well with other outer runners.



We can combine ContextManager with any other Handler. This is possible because ContextManager, within eff_flatmap, does not constrain the returned eff of the continuation.


Core DataTypes


central data structure which can capture Effects in a way that they can interact, while each is handled independently on its own


special Wrapper, which is completely peeled of again

Comparing to Identity, Identity{T} results in Identity{T}, while NoEffect{T} results in plain T.



special effectrunner which recognizes effecttypes used within eff and calls the effects in order, such that the first effect found will at the end be the most outer container, and the last different effect found will be the most inner container of the result value.



extract final value from Eff with all effects (but Identity) already run