Static Scoping of Persistents in KRL Modules


Summary

I made a scoping mistake in designing modules and it's time for KRL to come clean about that. This post describes the problem and what's changing to fix it.

Apollo 13 Command Module

With a title like that, I'm sure you just can't wait to dig into this blog post. The bottom line: I made a scoping mistake in designing modules and it's time for KRL to come clean about that. This post describes the problem and what's changing to fix it.

When I implemented KRL modules over a year ago, I was careful to ensure that module variables are statically scoped. For example, conside the following meta and global blocks from a KRL module:

meta {
  provides b
}
global {
  a = 5;
  b = function(x) { x + a };
}

Now suppose this module gets used with the alias test like so:

global {
  a = 16;
  p = test:b(5);
}

Will p have the value 10 or 21? If module variables are statically scoped, it will have the value 10 because the variable reference a in the function bound to b will refer to the a in play when it was declared (static) rather than when it is run (dynamic). You've probably surmised that if variables are dynamically bound then p will have the value 21.

There's only one tiny problem: regular variables are not the only things that can be bound to values in KRL. KRL also has a special kind of variable called a persistent variable that holds it's value across invocations of the ruleset. When I implemented modules, I more or less ignored and forgot about persistents. The result was that they ended up being dynamically scoped. This causes confusion for developers because not only don't KRL modules that reference persistents behave the way that we've come to expect from other programming languages, but they don't behave consistent with module variables in KRL. The's poor language design and I'm sorry.

Here's a detailed look at what I mean. Supposed, we had the following ruleset:

ruleset foo {
    meta {
        provides get_item
    }

    global { 
        get_item = function (k) {
            ent:elements{k};
        };
    }
    rule add_item {
        select when pds new_data_available
\tnoop();
        always {
            set ent:elements{event:attr("key")} event:attr("value");
            raise pds event new_item_added;
        }
    }
}

The provides declaration in the meta section clues us in that this ruleset is intended to be used as a module. Note that get_item() references an entity variable (ent:elements). An entity variable with the same name is mutated in the rule add_item. The idea is that this ruleset will function as a closure over the value in ent:elements allows the value to be set when the event pds:new_data_available is raised and retrieved via a function made available to other rulesets that use this one.

The problem is that persistent variables in modules are dynamically scoped whereas they're statically scoped when the ruleset is run because it contains a rule that responds to an event. Thus the two references to ent:elements in the preceding ruleset do not refer to the same place. The one in get_item() refers to the persistent ent:elements in the ruleset it runs in while the reference to ent:elements in the rule postlude refers to the persistent ent:elements in ruleset foo. That is clearly not what developers looking at this ruleset would expect and thus poor design.

In my defense, when I designed modules, I didn't even consider that some enterprising developer (yeah, I'm looking at you Sam Curren) would put rules in their modules. But they did and it proved to be very useful.

So, what we're going to do is fix this. We will make persisent variable references in modules statically scoped. If you're a Kynetx developer and you've written a module, you should ensure you haven't used persistents in module functions. If you have you're going to have to change your module to pass that value in as a function parameter, rather than simply referring to it. I doubt many people are relying on this, so we're just going to change it and help people fix their code later. Let me know if this is going to cause you too much heartburn.