Effect handlers#

An effect handler tells Crochet what should happen when an effect is performed. These handlers are specified in special handle ... with ... end blocks, where we wrap some piece of code and specify how we want certain effects to behave.

For example:

handle
  let A = perform counter.next();
  let B = perform counter.next();
  A + B
with
  on counter.next() do
    continue with 1;
  end
end

Here, we perform the effect counter.next() twice. Each of these times we’ll run the piece of code in the corresponding on handler. Our particular handler here only has one expression which says continue with 1. This means that, as a result of “performing” this effect, we’ll get the value 1. So both A and B will refer to 1.

Handling an effect#

What happens when we ask Crochet to perform an effect? Well, it will try to find the closest matching handler and execute it. For example:

handle
  let Result = perform greet.hello();
  Result + 1;
with
  on greet.hello() do
    show: "Hello!";
    continue with 1;
  end
end

Will look at its enclosing handle block, and then notice that we do have a handler for the greet.hello effect. So we execute it, showing Hello! to the user, and continue from where we stopped with 1. This value is associated with the variable Result, and we proceed to return Result + 1—that is, 2.

In a more visual way, we can think of handles and performs working as follows. First we see the handle block, so we keep track of what handlers it provides to us. In this example we have a handler for greet.hello, so our handlers look like this:

// Current handlers:
on greet.hello() do
  show: "Hello!";
  continue with 1;
end

We then move on to the piece of code inside the handle block:

// Current handlers:
on greet.hello() do
  show: "Hello!";
  continue with 1;
end

// Now running:
let Result = perform greet.hello();
Result + 1;

So the first thing we get to execute is the perform instruction. Here we see that it’s asking us to perform the greet.hello effect, and we just happen to have one in our current handlers list. So we move on to running the code inside of that handler instead. Here we show Hello! on the screen, and then continue executing from where we “performed” the effect, but with the value 1 instead. That is, it’s as if we had replaced the “perform” instruction with 1:

// Current handlers:
on greet.hello() do
  show: "Hello!";
  continue with 1;
end

// Now running:
let Result = 1;
Result + 1;

And so we move on to the next expression, Result + 1. Since Result is associated with 1, this gives us 1 + 1. The final result of this entire handle block is, then, 2.

Continuations and returns#

We’ve only glossed over the whole continue with business. Sadly, the way handlers work is complicated and requires a more detailed explanation.

Handlers and perform instructions work in tandem—as if they were in a conversation. When we “perform”, we ask a handler to take over the execution of the program, and so we do as the handler tells us to.

At any point, a handler may decide to give the stage back to the perform instruction, giving it an “answer”—this is where the continue with expressions come in. This continue with expression is called a “continuation”, and it knows exactly where in the program we stopped—hence it can “continue” that execution “with” some new value.

Most handlers will use the continue with expression to provide some kind of answer to its perform counterpart. This is similar to how values are returned in commands. That is, the following:

handle
  let Result = perform greet.hello();
  Result + 1;
with
  on greet.hello() do
    show: "Hello!";
    continue with 1;
  end
end

Is similar to:

command greet hello do
  show: "Hello!";
  1;
end

// And later:
let Result = greet hello;
Result + 1;

But instead of giving back an answer to the perform instructions, handlers can also give back an answer to the ``handle`` block itself. This is called a “return”, and it looks like the following:

handle
  let Result = perform greet.hello();
  show: "Here...";
  Result + 1;
with
  on greet.hello() do
    show: "Hello!";
    return 1;
  end
end

This works a bit different from our command execution. Instead of putting the 1 back where the perform instruction was, the return instruction replaces the entire handle block with that value. That is, we show Hello! on the screen, and then immediately have the result of the entire block be 1. We’ll never get to see Here... on the screen, because nothing else in that piece of code will be executed.

Nesting handlers#

Handlers can also be nested. For example:

handle

  handle
    perform num.one() + perform num.two();
  with
    on num.one() do
      continue with 1;
    end
  end

with
  on num.two() do
    continue with 2;
  end

  on num.one() do
    continue with 11;
  end
end

Here the inner handle block only defines a handler for the num.one effect, which continues the program with 1. The outer handle block defines a handler for num.two which continues the program with 2, and a handler for num.one which continues the program with 11.

So when we perform num.one(), the inner handler is the closest one, and we replace that perform instruction with 1. But when we perform num.two(), there’s no handler in the inner handle block, so the closest one is the handler in the outer handle block, which replaces the perform with 2. The result is then 1 + 2.

If the inner handle block did not have a handler for num.one, we would end up using the outer handler for it—ending up with 11 + 2.

Effect scoping#

We’ve seen that bindings are valid within certain regions of the code— which is their scope. Effects have a similar concept of being only valid within a certain region, but the way these regions work is a bit different.

Bindings have a “lexical scope”—the region where they’re valid can be seen in the source code. But effects have “dynamic scope”—the region where they’re valid depends on how the program runs.

For example, consider the case where we have some commands that eventually perform an effect:

command show: Text =
  perform display.show(Text);

command greet: Name =
  show: "Hello, [Name].";

Then if we have the following handle block:

handle
  greet: "Alice";
with
  on display.show(Text) do
    transcript display: Text;
    continue with nothing;
  end
end

The handler is not restricted to the code that can be seen in the handle block, but rather covers all code that is executed from there. This means that, even though the perform only happens in the show: _ command, it still sees our little handler for display.show, because it’s called from greet: _, which is in turn called from within the handle block.

On the other hand, in the following example, the call to show: _ that arises from greet: "Dorothy" would not see the handler, because this call does not originate from within the handle block:

handle
  greet: "Alice";
with
  on display.show(Text) do
    transcript display: Text;
    continue with nothing;
  end
end
greet: "Dorothy";

Missing handlers#

What happens if we don’t have a handler for a perform instruction? Crochet will still execute the code as normal, but when hitting the perform instruction the program would stop.

In interactive mode, this means that you’d have a chance of deciding how to continue the program, either by providing a value to continue the program with, or by returning a value from the handle block.

Reusable handlers#

Handle blocks define how effects behave in the program, but defining all of that behaviour in the handle block isn’t feasible or desirable. The package that defines the handle block might not even have access to the effect, for security reasons. And even when it does, it can easily lead to cases where the same handler code is repeated all over the place, making the use of effects a chore.

To address these two problems, Crochet allows defining reusable handlers. We can introduce one using the handler declaration:

handler show-on-transcript with
  on display.show(Text) do
    transcript display: Text;
    continue with nothing;
  end
end

We can then reference this handler within a handle block:

handle
  greet: "Alice";
with
  use show-on-transcript;
end

Any number of use declarations can be mixed with on ... declarations in the handle block, but effects handled by the block are not allowed to overlap. That is, it’s not possible to have:

handle
  greet: "Alice";
with
  use show-on-transcript;
  on display.show(Text) => continue with nothing;
end

Here both show-on-transcript and the inline handler are managing the same display.show effect, so it’s unclear which one Crochet should use. That’s thus disallowed.

Parameterised handlers#

Consider the case where it’s not entirely clear how a handler should behave from its own perspective—it needs a bit more of context. For example, when we display text, we might want to identify where that text came from. Parameterisation allows the user of the handlers to fill in this missing information.

Handler parameters are described in a similar way to command parameters:

handler show-on-transcript chapter: (Chapter is text) with
  on display.show(Text) do
    transcript display: "([Chapter]) [Text]";
    continue with nothing;
  end
end

We can use it like so:

handle
  greet: "Alice";
with
  use show-on-transcript chapter: "Prologue";
  on display.show(Text) => continue with nothing;
end

With would result in the following output:

(Prologue) Hello, Alice.

Note that, just like the commands greet: _ and greet: _ from: _ are distinct, the parameterised handlers are also distinct. That is, show-on-transcript, show-on-transcript chapter: _, and show-on-transcript chapter: _ act: _ would all be distinct handlers.

Handler initialisation#

Another aspect of reusable handlers is that they may contain custom initialisation code. That is, code that is executed before the handle block that contains it, and that may set up any necessary state for the handler. This is often coupled with parameterised handlers to make the internal state observable outside.

For example, a handler that collects text that uses display.show could be done as follows:

type io(input is cell<list<text>>, output is cell<list<text>>);

handler collect-show output: (IO is io) do
  let Output = IO.output;
with
  on display.show(Text) do
    Output <- Output value append: Text;
    continue with nothing;
  end
end

Initialisation code is executed before the handler is installed, so no effects performed within it will be handled by the handle block that’s using the handler. It is also executed in the sequence it appears in the containing handle block, so use a; use b; would first execute the initialisation code of a, and then the initialisation code of b.

Default handlers#

For non-parameterised effects, it might be useful to install them automatically at the global level. This allows pieces of functionality to be redefined through handle blocks, but without imposing the use of them on all programs, which might be useful in some cases.

Handlers can be marked as default like so:

handler atomic-memory-cell with
  ...
end

default handler atomic-memory-cell;

Default handlers are installed before executing the application entry-point, but after executing all prelude blocks. This means that preludes must be inherently pure.