Serious First Steps In UserTalk Scripting
by Matt Neuburg
Author of the book Frontier: The Definitive Guide

Prev | TOC | Next


Scope and With

Names and places

In the previous chapter, we saw that you can easily (perhaps unintentionally) change the value of entries in the database.

Access to the database is an implicit, powerful part of the UserTalk language. After all, that's why you can call any UserTalk verb, such msg() -- you're referring to an object in the database, the script object system.verbs.globals.msg.

In this chapter, we learn more about accessing the database, and how Frontier knows when you mean a database entry and when you mean a variable.

In the workspace.counter stub, we used variables "fromWhat" and "toWhat". But we could just as well have used database entries such as "workspace.fromWhat" and "workspace.toWhat", like this:

on counter (lowerLimit = 1, upperLimit = 10)
    if lowerLimit < upperLimit
        for n = lowerLimit to upperLimit
            msg (n)
            clock.waitseconds (1)
    else
        for n = lowerLimit downto upperLimit
            msg (n)
            clock.waitseconds (1)
    return "I am good at counting!"
if dialog.getInt("Count from?", @workspace.fromWhat) \
and dialog.getInt("Count to?", @workspace.toWhat)
    window.about()
    counter(workspace.fromWhat, workspace.toWhat)

Try it! Push the Run button; the script runs fine.

But now look in the workspace table, and you'll see two entries that weren't there before, called "fromWhat" and "toWhat", and sure enough they have whatever values you entered when the dialogs came up.

Conceptually, Frontier makes no distinction between variables and database entries. You can use a database entry as a variable in a script, as we have just done.

But Frontier does treat them differently in two respects.

First, variables are brought into existence only for the lifespan of their script, and then are automatically destroyed when the script is finished running; whereas, database entries persist until you explicitly delete them.

Second, database entries are visible to every script, because every script has the whole database at its disposal; whereas, variables have scope, meaning that they can be seen only by restricted parts of their own scripts.

We say, in computer parlance, that database entries are global, but variables are local.

The question, then, is: how local? This is the same as asking: what is the scope of a variable?

The scoping rule for variables is simple. It comes in two parts, though, so let's take them one at a time.

The Scoping Rule, Part 1

A variable cannot be seen outside the bundle where it was brought into existence.

A "bundle" is an indented group of commands. For instance, in workspace.counter, everything indented below and to the right of the "on" line is a bundle.

The variable "n" was brought into existence in that bundle. The implication is that after the line that starts "if dialog.getInt", the variable "n" doesn't exist, because that line and what follows are outside the "on" line's subordinate bundle.

Sure enough, if you add a line at the end of workspace.counter that says "msg(n)" and run workspace.counter, you'll get an error saying that there is no "n".

Unfortunately, the phrase "where it was brought into existence" isn't very helpful here, because it isn't entirely obvious just where the variable "n" was brought into existence.

That's because so far we've been allowing variables to be brought into existence implicitly; Frontier has been creating them for us when it sees we need them.

This is not considered wise UserTalk programming practice. The usual thing is to take charge of the situation, by bringing variables into existence ourselves, explicitly. That way, we know where they were brought into existence, and so we know their scope.

The way to bring a variable into existence explicitly is to declare it with a "local" statement.

This statement has two alternative syntax forms; you can either list variables in parentheses on the same line as the "local" keyword, or you can list them on their own lines, indented under the "local" keyword.

In either case, you can optionally give them an initial value if you like.

Here is workspace.counter, rewritten so that every variable is declared before being referred to (plus, we give fromWhat and toWhat initial values); try running it:

on counter (lowerLimit = 1, upperLimit = 10)
    local (n)
    if lowerLimit < upperLimit
        for n = lowerLimit to upperLimit
            msg (n)
            clock.waitseconds (1)
    else
        for n = lowerLimit downto upperLimit
            msg (n)
            clock.waitseconds (1)
    return "I am good at counting!"
local (fromWhat = 1, toWhat = 10)
if dialog.getInt("Count from?", @fromWhat) \
and dialog.getInt("Count to?", @toWhat)
    window.about()
    counter (fromWhat, toWhat)

In this particular example we are largely just making explicit what Frontier was doing implicitly for us anyway, but this is a good idea, because it helps fend off confusion about variable scope, making it clear how part 1 of the scoping rule applies to each variable we use.

(We do not declare "lowerLimit" and "upperLimit" local, because they are parameters; a parameter definition is in fact a form of local declaration already, and the bundle to which it applies is what's indented from the "on" line. Parameters thus automatically exist as locals within their own "on" bundle, and don't exist outside it.)

Because of this part of the scoping rule, it is common UserTalk programming practice to divide large programs, where feasible, into smaller bundles. This is often done, when the structure of the program doesn't do it already, with the "bundle" keyword, which itself performs no action, but is used as a placeholder to give the bundle something to be indented from.

In this way, variables of deliberately limited scope can be created; a variable declared "local" within a bundle ceases to exist after the bundle is over.

The Script menu provides a shortcut, the Bundle-ize command, which creates a "bundle" line before the selected line and then demotes subsquent lines to be indented beneath it.

Bundles are also generally useful as an organizational tool, because they give you something to collapse pieces of the script into, taking advantage of the outline structure of the script to shorten its display and to make it easier to move pieces around.

We are now ready for part 2 of the scoping rule, so here it is.

The Scoping Rule, Part 2

In case of two variables with the same name, the one with innermost scope takes precedence.

To see what this means, create and run the following script, workspace.scopeTester:

local (n = 1)
if n == 1
    local (n)
    for n = 1 to 3
        msg (n)
        clock.waitseconds (1)
msg (n)

(Notice that, as shown in our "if" line, the notation for testing equality is "==", not "=". This is a common source of error among beginners.)

When you run scopeTester, you will see the About Window count 1, 2, 3, then change back to 1. How can this be?

When we get to the "for" loop, we have declared a second local "n", inside the "if" bundle. Since this is "innermost" in comparison with the "n" declared at the start of the program, it is this "n", not the one at the start of the program, that is used in the "for" loop and displayed in the About Window.

Then, when we get to the last line of the script, we are outside the "if" bundle, and the innermost "n" no longer exists (it has "gone out of scope").

We are left with only the "n" created in the first line of the script. This "n" was initialized to 1 in the first line -- and its value was never changed thereafter, because the "n" in the "for" loop was a different "n"! So when we display "n" now, we see 1.

This little snippet is not a highly practical program. But the technique that it illustrates is common and valuable. Try to bring variables into existence for as brief a time as possible, that is, to declare them local as late and as deeply as their actual usage warrants. Such practice helps make scope clearer, and reduces the risk of accidental variable name conflicts.

Handler scope

Handlers also have scope, very much on a par with variables.

It is possible to define a handler inside a handler (often called a local handler), and this is very commonly done in order to package the parts of a script into units of single functionality, or to make a utility routine available to be called several times during a script.

Typically, what happens is, you have a piece of functionality that needs to occur at several different places in the script. So you abstract it out (remember, computers are good at abstraction) as a local handler.

For instance, in workspace.counter, these lines appear twice:

            msg (n)
            clock.waitseconds (1)

Here is a rewrite of workspace.counter where those lines are replaced by a call to a local handler, which contains them:

on counter (lowerLimit = 1, upperLimit = 10)
    on displayAndWait (n)
        msg (n)
        clock.waitseconds (1)
    local (n)
    if lowerLimit < upperLimit
        for n = lowerLimit to upperLimit
            displayAndWait (n)
    else
        for n = lowerLimit downto upperLimit
            displayAndWait (n)
    return "I am good at counting!"
local (fromWhat = 1, toWhat = 10)
if dialog.getInt("Count from?", @fromWhat) \
and dialog.getInt("Count to?", @toWhat)
    window.about()
    counter (fromWhat, toWhat)

Here, displayAndWait() is a little self-contained handler inside counter(). You might think: pretty silly. We didn't reduce the size of the program -- we increased it. And we added the overhead of two new verb calls too!

Good, you get an A in computer science. But you only get a C in UserTalk programming style, because this sort of thing is considered very elegant and valuable (or, as we say in technical jargon, "cool"). So get with it!

Things to notice: displayAndWait(), like "n", is not visible to anything after the "if dialog.getInt" line. Furthermore, displayAndWait()'s "n" is different from counter()'s "n", because, being a parameter, it is automatically local to just what's inside displayAndWait().

Now here's something really cool. If you wanted to get clever with "n" you could write instead:

on counter (lowerLimit = 1, upperLimit = 10)
    local (n)
    on displayAndWait ()
        msg (n)
        clock.waitseconds (1)
    local (n)
    if lowerLimit < upperLimit
        for n = lowerLimit to upperLimit
            displayAndWait ()
    else
        for n = lowerLimit downto upperLimit
            displayAndWait ()
    return "I am good at counting!"
local (fromWhat = 1, toWhat = 10)
if dialog.getInt("Count from?", @fromWhat) \
and dialog.getInt("Count to?", @toWhat)
    window.about()
    counter (fromWhat, toWhat)

Here, "n" is never explicitly handed to displayAndWait(). Yet displayAndWait() still knows what the value of "n" is. How? It's because, being called inside the same bundle as "n" and after its "local" declaration, displayAndWait() can see "n" directly. We say that "n" is global to the handler displayAndWait().

This is important to be clear about, because it's a major programming technique: a local handler has direct access to all local variables within whose scope it is called. They are global to it.

That fact is good and bad.

It's good because you don't have to hand those variables to the handler as parameters; there's a certain amount of internal overhead associated with parameter passing, and it's nice to be able to avoid this.

It's bad because the handler has the power to change those variables! This might be exactly what you want to do, of course, but it's another one of those cases where with increased power comes increased responsibility.

Watch your variable names in a local handler to make sure you aren't accidentally tromping a variable from the surrounding scope; to be safe, declare as local in the handler those variables you intend should be local in the handler.

Partial pathnames

As we saw, database entries are global. They don't have scope; they are visible to all statements in all scripts.

They do, however, have pathnames which can get long and cumbersome. To make it more convenient to refer to commonly used database entries, UserTalk maintains a repertory of partial pathname prefixes, places at some depth inside the database where it will look for database entries to which you don't provide a full pathname.

This is why it is possible to call a built-in verb like dialog.getInt() by saying "dialog.getInt()", rather than your having to say "system.verbs.builtins.dialog.getInt()".

The standard partial pathname prefixes are listed at system.paths; whenever you refer to a database entry using a partial pathname, Frontier looks for it in each of these locations in turn. When it gets to path03, which is @system.verbs.builtins, it finds dialog.getInt. That's how it knows what "dialog.getInt()" means.

With... statements

You can also define a partial pathname prefix temporarily in a script yourself, using a "with" statement.

As with other structural keywords we've met, the commands to which the "with" applies are denoted by being indented below the "with" line.

For example, we might rewrite workspace.counterCaller as:

with workspace
    theAnswer = counter (4, 8)
    dialog.notify(theAnswer)

This example is not very realistic, but it illustrates the use of "with".

All partial pathnames used in the "with" statement's indented bundle are initially sought by prefixing the name specified in the "with" statement. There is no workspace.theAnswer, and it isn't anywhere in the places named by the system.paths prefixes, so theAnswer is ultimately taken to be a local variable. But there is a workspace.counter, so our use of the name "counter" is interpreted as if we had said "workspace.counter".

Incidentally, this shows another reason for declaring local variables. There's an insidious trap in this little script, which has been well described by Scott Lawton in a justly famous note to the Frontier mailing lists.

If you were to run the above version of workspace.counterCaller, and there did happen to be a database entry called workspace.theAnswer, it would be this, not a local variable, that would receive the result of the call to counter. Frontier won't use a "with" to create an entirely new database entry, but it will happily modify or use an existing database entry. We could end up accidentally tromping the value of some database entry!

To be absolutely safe, we should declare the local. If we wish to keep workspace.theAnswer (if it exists) from being overwritten, we must declare the local inside the "with", like this:

with workspace
    local (theAnswer)
    theAnswer = counter (4,8)
    msg(theAnswer)

Writing our script like this wouldn't have the same effect:

local (theAnswer)
with workspace
    theAnswer = counter (4,8)
    msg(theAnswer)

This would still tromp any existing workspace.theAnswer, because the scope of the "with" is inside the scope of the "local", and takes precedence.

Driving with with

The commonest real-life use of "with" is when UserTalk is to drive other applications.

Take, for instance, Eudora. The verbs that drive other applications are all located at system.verbs.apps, and the ones that drive Eudora are thus at system.verbs.apps.Eudora; since we already have a partial pathname to system.verbs.apps, it suffices to call Eudora.createMessage(), Eudora.queue(), and so on.

So it is common, when giving several commands in a row to Eudora, to wrap them in a "with Eudora" statement and then just call createMessage(), queue(), etc.

From the school of hard knocks

A common error involving partial pathnames is forgetting to use "with" or a pathname altogether.

By this I mean that one becomes so used to the idea of having implicit partial pathnames that one forgets that one has no implicit partial pathname to this particular database entry. For example, it is all too likely that one will accidentally write:

if dialog.getInt("Count from?", @fromWhat) \
and getInt("Count to?", @toWhat)

Something about the way the human mind is constructed makes us assume subconsciously that because we just told Frontier on the first line what "getInt" we're talking about, we don't have to tell it again on the second line. But that's not true!


Prev | TOC | Next

All text is by Matt Neuburg, phd, matt@tidbits.com.
For information about the book Frontier: The Definitive Guide, see my home page:
http://www.tidbits.com/matt
All text copyright Matt Neuburg, 1997 and 1998. ALL RIGHTS RESERVED.
No one else has any right to copy or reproduce in any form, including electronic. You may download this material but you may not post it for others to see or distribute it to others without explicit permission from the author.
Downloadable versions at http://www.ojai.net/matt/downloads/scriptingTutorial.hqx and http://www.ojai.net/matt/downloads/scriptingTutorial.zip.
Please do not confuse this tutorial with a certain other Frontier 5 tutorial based upon my earlier work.
This page created with Frontier, 2/11/2000; 6:59:03 PM.