← Back to lessons

52 Effect Handlers

Hard

Effect 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.

effectHandlers.cj
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