Scoping rules and their consequences

This section describes the scoping rules that GCL is built on, the design decisions that have been made and the consequences of these decisions. At first glance, GCL might require you to type “too much”, or ask you to type things in weird places during tuple composition, but this has been done for good reason: to keep GCL models easy to reason about, even when they grow large.

Lexical Scope

References in GCL are lexically scoped, where each file starts a new scope and each tuple forms its own subscope.

“Lexically scoped” means that only variables that are declared in the current tuple, or any of its parent tuples, are visible in expressions.

For example:

blue = '#0000ff';

knight = {
    armor = 'chain_mail';
};

lancelot = knight {
    favorite_color = blue;  # Visible

    helmet = armor;  # Not visible!
};

Note that in the lancelot tuple, the assignment of armor to helmet will fail, because even though a value for armor is mixed in from the knight tuple, that value is not lexically visible!

Behaving like this is the only way to avoid “spooky action at a distance”. Let’s assume GCL did not behave this way, and we would always take the value from the current closest enclosing tuple. Then what would happen if someone else, a colleage, inadvertently added a key named blue onto the knight tuple?

blue = '#0000ff';

knight = {
    armor = 'chain_mail';
    blue = 'smurf';
};

lancelot = knight {
    favorite_color = blue;  # Whoops! I wanted '#0000ff' but I got 'smurf'!
};

Especially when the tuples invoved are far apart, such as in different files, these effects become nearly impossible to see and debug. Lexical scoping prevents that.

Valueless keys are input parameters

To be able to refer to a variable, an expression needs to be able to “see” it. This means that it must be in the same tuple or an enclosing tuple of the one the expression is in.

If you want to pass a variable then from one tuple to another, or one file to another, the receive file must have an “empty declaration” of that variable somewhere. For example:

# http.gcl
server = {
    dirname;
    www_root = '/var/www/' + dirname;
};

And use it as follows:

# main.gcl
http = include 'http.gcl';
pics_server = http.server {
    dirname = 'pics';
};

A file behaves like a tuple, so if you need to refer to the same value a lot in a file, you can make it a file-level parameter as well:

# http.gcl
port;

server = {
    host = '0.0.0.0';
    bind = host + ':' + port;
};

And use it as:

# main.gcl
https = include 'http.gcl' { port = 443 };
pics_server = https.server {
    ...
};

In this example, we make https an instantiation of the http library with a variable pre-bound. This is common pattern in large GCL models, and can be thought of as “partial application” of a collection of tuples.

As you can see, the downside of the design decision of lexical scoping is that you need to type more, because you need to declare all the “empty variables” that you’re expecting to be using. On the plus side, you know exactly what you’re referring to, and tuples that are you are going to be mixed into can not affect the binding of your variables in any way.

The ‘inherit’ keyword

Let’s say you want to copy a variable from an outer tuple onto a particular tuple under the same name. Let’s say you want to write something like this:

base_speed = 3;
motor = {
    base_speed = base_speed;  # Recursive reference
    speed = base_speed * 2;
};

First of all, you may not need to do this! If you just wanted the variable speed set to the correct value, there’s no need to copy base_speed onto the motor tuple. You can easily refer to the base_speed variable directly.

If you still want to copy the variable, the code as written won’t work. base_speed on the right refers to base_speed on the left, whose value is base_speed on the right, which refers to base_speed on the right, and so on.

To solve this, you can do one of two things: rename the variable, or use the inherit keyword (which copies the variable from the first outer scope that contains it while ignoring the current scope).

So either do:

base_speed = 3;
bspd = base_speed;
motor = {
    base_speed = bspd;
    speed = base_speed * 2;
};

Or do:

base_speed = 3;
motor = {
    inherit base_speed;
    speed = base_speed * 2;
};