Controllers

Sometimes, when designing an API, we want to be able to customize Monte’s behavior while retaining the general Monte idioms for values and layouts. Controller syntax lets us change behavior of code blocks in a safe and coherent fashion.

How to Implement a Controller

Controller Expansion

Suppose that we have a standard if-expression:

if (cond()) {
    advance()
} else {
    fallback()
}

Now, suppose that we wished to customize this. We could define a controller named ifController, and then call it with very similar syntax:

ifController (cond()) do {
    advance()
} else {
    fallback()
}

This expands roughly to the following:

(ifController :DeepFrozen).control("do", 1, 0, fn {
    [[cond()], fn { advance() }]
}).control("else", 0, 0, fn {
    [[], fn { fallback() }]
}).controlRun()

We see that controllers must be DeepFrozen, and that each code block, which we’ll call a “lambda-block”, corresponds to a .control/4 call, with a .controlRun() to indicate the end of blocks.

Control with Lambda-Blocks

The power of controllers is locked within the lambda-blocks. Each block is a function which returns an [args, lambda] pair. The controller can choose how many times it wants to call the block, and similarly, the block can return new arguments every time it is called. Indeed, note above that cond() is called every time its containing lambda-block is called.

What are the other arguments to .control(verb :Str, argCount :Int, paramCount :Int, block)? The control verb is the bare word preceding each block. The argument count specifies how many arguments will be returned by the block. Where are the parameters?

Let us imagine another hypothetical controller:

m (action) do x { f(x) }

In this situation, x is the one and only parameter, and so the controller receives a parameter count of 1.