UserLand Software
Powerful, cross-platform web scripting.
 

Scope and With

Frontier Scripting Tutorial

About This Tutorial

What Does Frontier Do?

Keywords, Handlers, Verbs, and Calls

Loops, Variables, Parameters, and Conditionals

The Handler Rule

Returns, Addresses, and Dereferencing

Scope and With

Strings and Files

Outlines and Tables

Running, Debugging, and Getting Help

Datatypes

A Real-Life Problem

String Parsing and Substitution

Manipulating Files and Folders

Final Touches

Scripts are objects

Recall from our earlier discussion that Frontier doesn't really draw any distinction between a script and any other object living in the database.

Verbs can have names that are like the names of database entries because verbs are database entries.

When you say "workspace.Counter (3, 6)", you're referring to the database entry workspace.Counter; it's the parentheses that tell Frontier that what you want to do is call it as a verb.

Variables are objects

Much the same thing is true of variables.

In the 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, workspace.fromWhat and workspace.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 scripter's 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 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" 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" is not very helpful, because it isn't always obvious where a variable 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 Frontier scripting 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 don't exist outside their own "on" bundle.)

Because of this part of the scoping rule, it is common Frontier scripting practice to divide large scripts, where feasible, into smaller bundles. This is done 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.

(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 "==" and 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 script. But in general, locals are good. 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.

Handlers have scope

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

It is possible to define a handler inside a 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.

For instance, here is a rewrite of Counter where the expressions common to both "for" loops are taken out into a utility handler:


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.

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.

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; it doesn't have to be, because, being inside the same bundle as "n" and after its "local" declaration, displayAndWait can see "n" directly.

This is important to be clear about, because it's a major scripting technique: a local handler has direct access to all local variables within whose scope it is declared.

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 believe are local in the handler.

Database entries don't have scope

As we saw, database entries are global.

They do, however, have pathnames which can get long and cumbersome. To make it more convenient to refer to database entries, Frontier maintains a repertory of partial pathnames, 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.dialogs.getInt."

The standard partial pathnames are listed at system.paths; whenever you refer to a database entry, Frontier looks for it in each of these locations in turn. When it gets to path03 it finds dialog.getInt.

With... statements

You can also define a partial pathname 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.

(You can nest "with" statements, or you can use a shortcut by listing the partial pathnames you want to use. Frontier searches starting at the deepest "with," moving outwards.)

For example, we might rewrite workspace.CounterCaller as:


with workspace
   theAnswer = Counter (5)
   msg (theAnswer)

This example is not very realistic, to be sure; after all, we've saved nothing by writing CounterCaller in this way.

The above example, small and innocent-looking as it is, demonstrates an insidious trap that 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 CounterCaller, and there just happened 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 one, and since the "with" is deeper than the scope of the local, Frontier checks for the existence of workspace.theAnswer before looks for a local called "theAnswer."

To get around this and be absolutely safe, you may wish to declare a new local inside the "with"," like this:


with workspace
   local (theAnswer)
   theAnswer = Counter (5)
   msg (theAnswer)

This way of writing the script keeps workspace.theAnswer (if it exists) from being overwritten.

By the way, 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!

PreviousNext

   

Site Scripted By Frontier © Copyright 1996-98 UserLand Software. This page was last built on 2/10/98; 1:26:59 AM. It was originally posted on 4/15/97; 8:53:18 PM. Webmaster: brent@scripting.com.

 
This tutorial was adapted for Frontier 5 by Brent Simmons, from the Frontier 4 scripting tutorial written by Matt Neuburg.