ExtensibleEffects Public API
Usage and Syntax
Autorun
ExtensibleEffects.@syntax_eff
— Macro@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 function | description |
---|---|
ExtensibleEffects.eff_applies(handler::Type{<:Vector}, effectful::Vector) = true | specify 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_noautorun
— Macro@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.noautorun
— Functionnoautorun(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
ExtensibleEffects.effect
— Functionmark a value as an effect
ExtensibleEffects.noeffect
— Functionmark a value as no effect, but plain value
Explicit use of handlers
ExtensibleEffects.runhandlers
— Functionrunhandlers(handlers, eff)
runhandlers((Vector, Option), eff)::Vector{Option{...}}
run all handlers such that the first handler will define the most outer container
ExtensibleEffects.@runhandlers
— Macro@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_applies
— FunctionExtensibleEffects.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
— FunctionExtensibleEffects.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
— FunctionExtensibleEffects.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_autohandler
— Functioneff_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.runhandler
— Functionrunhandler(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, ...
ExtensibleEffects.runhandler_not_applies
— Functionrunhandler_not_applies(handler, eff)
if your handler does not apply, use this as the fallback to handle the unknown effect.
for developing composable effect-handler-macros
ExtensibleEffects.@insert_into_runhandlers
— Macro@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.WriterHandler
— TypeWriteHandler(pure_accumulator=neutral)
Handler for generic Writers. The default accumulator works with Option values.
Callable
ExtensibleEffects.CallableHandler
— TypeCallableHandler(args...; kwargs...)
Handler for functions, providing the arguments and keyword arguments for calling the functions.
ExtensibleEffects.@runcallable
— Macro@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.StateHandler
— TypeStateHandler(state)
Handler for running State. Gives the initial state.
ExtensibleEffects.@runstate
— Macro@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.ContextManagerHandler
— TypeContextManagerHandler(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.@runcontextmanager
— Macro@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.@runcontextmanager_
— Macro@runcontextmanager_(eff)
like @runcontextmanager(eff)
, but immediately runs the final ContextManager
ExtensibleEffects.ContextManagerCombinedHandler
— TypeContextManagerCombinedHandler(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.Eff
— Typecentral data structure which can capture Effects in a way that they can interact, while each is handled independently on its own
ExtensibleEffects.Continuation
— Typeonly for internal purposes, captures the still unevaluated part of an Eff
ExtensibleEffects.NoEffect
— Typespecial 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.autorun
— Functionautorun(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.
ExtensibleEffects.NoAutoRun
— TypeWrapper to indicate that a given handler should not be handled within autorun
runlast
ExtensibleEffects.runlast
— Functionrunlast(eff::Eff)
extract final value from Eff with all effects (but Identity) already run
ExtensibleEffects.runlast_ifpossible
— Functionrunlast_ifpossible(eff::Eff)
like ExtensibleEffects.runlast
, however if the Eff is not yet completely handled, it just returns it.