52 Effect Handlers
HardEffect handlers are non-local control operations that allow programs to pause and resume execution. They are similar to exceptions but extend them in a sense that you
can resume the code flow from the line where the effect got performed if you wish, while for exceptions the code flow only continues downward. Exceptions are thrown, while
effects are performed. Exceptions are caught, while effects are handled. Effect handlers allow smooth implementation of different things, like dependency injection,
custom concurrency, memoization, etc.
Every effect must implement Command<T> interface, where T is the type of value that is returned by resume.
We can also define a custom handler. The way it works is that if an effect is not handled and has defaultImpl, then that method gets called. If effect is not
handled but there is no defaultImpl, exception is thrown (demonstrated in the last example).
In addition to changing control flow, perform can return a value and resume can take an argument
Effect handlers have similar behaviour to exceptions when it comes to handler resolution. When we perform an effect, the runtime searches up the stack for the first
handler that handles the type of effect we have performed. However, the resume command returns the flow of control at the point where the effect was performed rather
than exit the handler as is the case with exceptions.
import stdx.effect.Command
class Example <: Command<Int64> {}
class One <: Command<Unit> {}
class Two <: Command<Unit> {}
class Three <: Command<Unit> {}
class Default <: Command<Unit> {
public func defaultImpl() {
println("defaultImpl called")
}
}
main() {
// Simple Example With Producing a Value
try {
print("one ")
var foo = perform Example()
print("three: ${foo} ")
} handle (_: Example) {
print("two ")
resume with 99
}
println("four")
// Nested Effects and Dynamic Binding
try {
// Handled with outer handler
perform One()
try {
// Handled with inner handlers
perform One()
perform Two()
// Handled with outer handler as it's not available here
perform Three()
} handle (_: One) {
println("One inner")
resume
} handle (_: Two) {
println("Two")
resume
}
// After perform Three() is handled from line 50 it is not resumed,
// which means we will never reach this statement
println("Unreachable")
} handle(_: One) {
println("One")
resume
} handle(_: Three) {
println("Three")
}
// Default Handlers
// It is not handled here so default handlers gets called
perform Default()
try {
// Handler below gets called
perform Default()
} handle(_: Default) {
println("handler executed")
}
// The effect neither has a default handler not is handled
// explicitly, so an exception is thrown
perform Two()
}
// Output:
// one two three: 99 four
// One
// One inner
// Two
// Three
// defaultImpl called
// handler executed
// An exception has occured:
// Unhandled Command: Unhandled command