ExtensibleEffects Public API

Usage and Syntax

Autorun

ExtensibleEffects.@syntax_effMacro
@syntax_eff begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b
end

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.

Examples

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

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
end

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.
ExtensibleEffects.@syntax_eff_noautorunMacro
@syntax_eff_noautorun begin
  a = [1,2,3]
  b = [a, a*a]
  @pure a, b
end

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.

Examples

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

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
end
ExtensibleEffects.noautorunFunction
noautorun(handlers...)::Function

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

Example

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

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

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

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

ExtensibleEffects.@runhandlersMacro
@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.

Example

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

Interface

Core Interface

ExtensibleEffects.eff_appliesFunction
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_pureFunction
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_flatmapFunction
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.

Parameters

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}

Return

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)

ExtensibleEffects.eff_autohandlerFunction
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

ExtensibleEffects.runhandlerFunction
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

ExtensibleEffects.@insert_into_runhandlersMacro
@insert_into_runhandlers outer_handler @syntax_eff begin
  # ...
end

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
      # ...
    end
  end
end)

@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)
  end))
end

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

Writer

ExtensibleEffects.WriterHandlerType
WriteHandler(pure_accumulator=neutral)

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

Callable

ExtensibleEffects.CallableHandlerType
CallableHandler(args...; kwargs...)

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

ExtensibleEffects.@runcallableMacro
@runcallable(eff)

translates to

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

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

State

ExtensibleEffects.@runstateMacro
@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.

ContextManager

ExtensibleEffects.ContextManagerHandlerType
ContextManagerHandler(continuation)

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.

ExtensibleEffects.@runcontextmanagerMacro
@runcontextmanager(eff)

translates to

ContextManager(function(cont)
  @insert_into_runhandlers ContextManagerHandler(cont) eff
end)

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

ExtensibleEffects.ContextManagerCombinedHandlerType

ContextManagerCombinedHandler(otherhandler)

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.

Internals

Core DataTypes

ExtensibleEffects.EffType

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

ExtensibleEffects.NoEffectType

special Wrapper, which is completely peeled of again

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

autorun

ExtensibleEffects.autorunFunction
autorun(eff)

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.

runlast

ExtensibleEffects.runlastFunction
runlast(eff::Eff)

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