Execution models#

Computations drive what Crochet can do, but how does Crochet decide stuff like the order in which these computations are executed? That’s what execution models help answer. Crochet has a few different models for executing computations, and in this section we discuss all of their details.

Immediate evaluation#

The default model Crochet uses to execute programs is called to execute expressions immediately, often called “eager evaluation”. That is, when a program consists of multiple expressions, Crochet will execute them one at a time, in the order they’re written, only moving to the next one once the current one finishes executing.

For example, in this piece of code:

player do-something;
player do-another-thing;

Crochet would first execute player do-something, and once that finishes, it would execute player do-another-thing. As if it was following a step-by-step recipe for cooking something.

Evaluation of sub-expressions#

Whenever sub-expressions are involved, Crochet will execute the sub-expressions first before it executes the outer expressions. For example:

2 + (3 - 4)

Here, Crochet would first execute (3 - 4), which yields -1, and then it would execute 2 + (-1), which yields 1. When multiple sub-expressions are involved, Crochet executes things from left-to-right:

(1 + 2) + (3 - 4)

Here we first execute (1 + 2), then (3 - 4), then 3 + (-1). Or, in a more visual manner, this would be the steps Crochet takes:

(1 + 2) + (3 - 4)

// after executing one step:
3 + (3 - 4)

// after executing one step:
3 + (-1)

// after executing one step:
2

When does this matter?#

When these expressions are “pure”—that is, when the steps we take can’t be observed—as is usually the case in arithmetic, the order really doesn’t matter. But not all expressions are pure.

Consider the case where a command can display some dialogue to the player of the game. If we have a piece of code like:

dialogue show: <<Claris: "May I have some coffee?">>;
dialogue show: <<Ane: "I will brew it shortly.">>;

Then the player would see this on the screen:

Claris: “May I have some coffee?”

Ane: “I will brew it shortly.”

The order is very important here, because if we swap these dialogue lines the entire story ceases to make sense!

We discuss this in more details in the Effects chapter, where we introduce a tool to better reason and describe these cases.

Partial programs#

By default, Crochet will execute expressions immediately, one after the other, in the order they’re written. This is not always desirable.

For example, imagine the case where we want to keep only numbers that are divisible by 2 in a list of numbers. This rule can be expressed as the Crochet program:

Item is-divisible-by: 2

However, we cannot execute this program as is because we don’t know what Item we’re talking about. So, for these cases, we want abstractions. A way of capturing a Crochet program—perhaps with some additional requirements—and then passing it around for others to execute.

This is how the _ keep-if: _ command on lists works, in fact. It takes in a Crochet program and for each item in the list, asks that program if the item should be kept in the result. We could write it like this:

[1, 2, 3, 4] keep-if: (Item is-disibible-by: 2)

But if we do so, we still run into the same problem as before. Item is-disivible-by: 2 will be executed before we execute _ keep-if: _, and thus we won’t know what Item is again.

The preferred solution for this is to use “partial application”. That is, instead of specifying all of the arguments to a command, we only specify those that we know, and leave some “holes” for others to fill in later—thus we capture a Crochet program that we can pass around for others to execute. We specify holes by placing an undescore (_) character where the argument would go:

[1, 2, 3, 4] keep-if: (_ is-divisible-by: 2)

Inside of _ keep-if: _ we’ll then fill that hole with some value. So if the partial program was captured with the name Predicate, this means that _ keep-if: _ would fill the hole through the syntax Predicate(1):

let Predicate = (_ is-divisible-by: 2);
Predicate(1);

// Equivalent to:
1 is-divisible-by: 2;

Applications with multiple holes#

We can have multiple holes in a command application. This means that we’ll end up with a partial program that has multiple requirements in order to work. Requirements are filled from left-to-right.

For example:

let Between = 5 is-between: _ and: _;
Between(1, 10);

// Equivalent to:
5 is-between: 1 and: 10;

Holes can be also used when we have a partial program with multiple requirements, if we’re only fulfilling some of them. For example:

let Between-for-5 = 5 is-between: _ and: _;
let Between-for-5-and-10 = Between-for-5(_, 10);
Between-for-5-and-10(1);

// Equivalent to:
5 is-between: 1 and: 10;

Anonymous partial programs#

Partial programs can be created efficiently by writing a command, and then specifying only some of its arguments. But sometimes we may have a slightly larger partial program that we don’t really want to go to the trouble of naming.

In these cases, Crochet allows anonymous partial programs to be defined within curly braces, and naming all of the holes. For example, if we wanted to keep only numbers that are larger than 5, after being multiplied by 2, we could write:

[1, 2, 3, 4] keep-if: { Number in (Number * 2) > 5 };

Which would be the equivalent of the following:

command integer double-is-greater-than: Number =
  (self * 2) > Number;

// And then used as:
[1, 2, 3, 4] keep-if: (_ double-is-greater-than: 5);

Evaluation time in partial programs#

It’s important to note that, when a program is partially specified with holes, all of the non-hole arguments are fully executed before creating the partial program—so it’s their resulting values that are carried around along with the program.

For example, consider a case where we have a command that takes two pieces of text, some character name and the line they are saying, and then formats it appropriately and shows that to the player:

command A says: B do
  show: <<[A]: "[B]">>;
end

If we were to use it like this:

"Alice" says: "Curiouser and curiouser...";

We would get:

Alice: “Curiouser and curiouser…”;

We could then construct a partial program that captures Alice saying things, to get the same effect:

let Alice-Says = "Alice" says: _;

Alice-Says("Curiouser and curiouser...");

Now, imagine that instead of the Alice’s name, we had a command that shows a short description of Alice before showing what she’s saying:

singleton alice;

command alice describe do
  show: "Alice is a young girl in a blue dress.";
  "Alice";
end

If we had the same program as before, but using this alice describe command, we’d have end up with the following:

let Alice-Says = (alice describe) says: _;

show: "Introduction.";
Alice-Says("Curiouser and curiouser...");
Alice-Says("What use is a book without pictures or conversations?");

What we would end up is the following output:

Alice is a young girl in a blue dress.

Introduction.

Alice: “Curiouser and curiouser…”;

Alice: “What use is a book without pictures or conversations?”;

So, as we see, alice describe has been executed right when we made the partial program. Only the "Alice" piece of text remained, which was used when we applied that partial program.

Things work differently when we have anonymous partial programs. No part of an anonymous partial program is executed until the partial program is applied—and then, the entire program is always executed. If we had an anonymous partial program instead, as follows:

let Alice-Says = { What in (alice describe) says: What };

show: "Introduction.";
Alice-Says("Curiouser and curiouser...");
Alice-Says("What use is a book without pictures or conversations?");

We would end up with the following output:

Introduction.

Alice is a young girl in a blue dress.

Alice: “Curiouser and curiouser…”;

Alice: “What use is a book without pictures or conversations?”;

Delayed programs#

Sometimes we have a complete program, but we want to delay their execution until a later point in time. Crochet calls these “delayed programs”.

There are two kinds of delayed programs. The regular delayed programs are like an anonymous partial program—indeed they use the same syntax—, but they have no requirements to be fulfilled.

The second kind of delayed programs are “lazy programs”. Lazy programs are a bit special in that they can only be executed once.

Regular delayed programs#

A regular delayed program has the same syntax as an anonymous partial program, but without any requirements—because the program is already complete:

let Hello = { show: "Hello!" };

Here, Hello refers to a delayed program. When we execute this program, nothing will happen. In order to make things happen, we need to apply the delayed program as before:

Hello();

Will output:

Hello!

Delayed programs in this form can be indeed applied multiple times, and every time we apply them we’ll execute the entire delayed program again, causing any of its effects to also happen again:

Hello();
Hello();

Will output:

Hello!

Hello!

Lazy delayed programs#

Lazy delayed programs are a bit special. They’re described with the special lazy syntax:

let Hello = lazy (show: "Hello!");

Just as before, nothing will have happened at this point. We’ll just have a delayed program referred to by the Hello name.

In order to apply a lazy delayed program, we use the special syntax force:

force Hello;

Will output:

Hello!

We can force lazy delayed programs multiple times, but they’ll only be executed once:

force Hello;
force Hello;

Will output:

Hello!

The value of lazy delayed programs is rather in capturing computations that can be expensive. You want to delay doing that as much as possible, and when you do, you don’t want to do it more than once. For example:

let Fibonacci-of-55 = lazy (55 fibonacci);

show: (force Fibonacci-of-55);
show: (force Fibonacci-of-55);

This will output:

139 583 862 445

139 583 862 445

But only the first time we force the lazy program will we actually compute the Fibonacci of 55. When we do so we’ll remember that result, so whenever it’s forced again the remembered result can be returned immediately.