This page uses CSS, and renders properly in the latest versions of Microsoft Internet Explorer and Mozilla.
This is Frontier: The Definitive Guide by Matt Neuburg, unabridged and unaltered from the January 1998 printing.
It deals with the freeware Frontier 4.2.3; for commercial Frontier 8, look here, and for the inexpensive Radio UserLand, look here. See my Web site for information.
Those wishing an offline or printed copy may purchase the book, which is still in print.
Copyright 1998 O'Reilly & Associates, Inc. ALL RIGHTS RESERVED. Reprinted with permission.
TOP UP: UserTalk Basics NEXT: The Scope of Variables and Handlers

6

Referring to
Database Entries

The database is a major resource of Frontier, and just about any UserTalk script will likely refer to some entry in it.

An entry in the database is a lot like a variable. It has a name, and it has a value, and you are free to get or set that value. Operations that can be performed with variables can all be performed with database entries, and vice versa. Indeed, a database entry really is a kind of variable. Nevertheless, there are differences, and it is often useful to draw a distinction between the two. When we need to lump them together, we can say "database entry or variable," or simply object.

This chapter describes the differences between variables and database entries, and explains how to refer to database entries with UserTalk.

How Variables and Database
Entries Differ

In Example 4-1, s is a variable:

s = "Hello, World!"
dialog.notify(s)

In the following script, workspace.s is an entry in the database.

Example 6-1 Assignment into the database
workspace.s = "Hello, World!"
dialog.notify(workspace.s)

Running each of these two scripts would appear to give just the same result - identical dialogs appear - but there is an important difference. After running the second script, examine the workspace table and you will find that an entry called s has been created within it, and that its value is the string "Hello, World!" This highlights one important difference between variables and database entries: Database entries are persistent , meaning that they continue to exist, maintaining their values, even when no script is running.1 Variables, on the other hand, are temporary; in the first script in this chapter, the variable s is destroyed when the script finishes executing.

Another difference between variables and database entries is that database entries are global , meaning that they can be referred to from anywhere. There is only one workspace.s (if there is any at all), and any script can speak of it unambiguously. Hence it is possible for one script to assign a value to workspace.s and another to use that value subsequently. Indeed, this is an important technique for making a value available amongst scripts.

You might wonder: If variables and database entries are alike, how does Frontier know which is being referred to by a name? For one thing, in our previous examples, at least, the variable name contained no dot, and the database entry name did. Generally speaking, variable names cannot contain a dot (they can be forced to do so, but it's a poor idea), so there's a clue Frontier uses. But it is not the only clue. The question is a major one, and is discussed in detail, partly in this chapter, partly in the next.

Being Careful with the Database

Since a script is allowed to change the value of any entry in the database, you should proceed with great caution - the same caution with which you would consider changing a value in the database manually, but even more so, because a script, once set going, is out of the user's immediate control. (On the other hand, you may get values from the database without hesitating; we saw an example of this in Example 4-6, where the value stored at user.name was used to supply a default in a dialog.)

Overly drastic accidental damage to the database is checked, to some degree, by limits on what Frontier is willing to do to the database in response to an assignment command. In Example 6-1, a database entry workspace.s was created in response to an assignment statement; implicit creation is an important feature of assignment. But if we had said:

workspace.ourSubtable.s = "Hello, World!"

and if the workspace table did not contain a subtable called ourSubtable, a runtime error would have resulted: Frontier refuses to create a table implicitly like this. (Of course, there are verbs to create a table explicitly.)

Frontier will also balk at an attempt to replace an existing non-scalar database entry or variable (for instance, a script object, a wptext object, or a table) with a scalar value (such as 7 or "Hello").

Still, Frontier will happily, in response to an assignment command, replace one scalar with another, or a scalar with a non-scalar, or a non-scalar with a non-scalar, altering the value and the datatype of the object to obey the assignment. So assignment has plenty of power to do damage. And later, we will meet verbs that can overcome all these safety limitations. So be careful what you say!

Calculated Names

In referring to a database entry or variable from a script, it is sometimes necessary to calculate part or all of its name at runtime. Consider, for instance, the business of referring to entries in your part of the people table. When Frontier is first obtained and started up, a dialog asks you for your initials. A subtable is then automatically created inside the people table; the name of the subtable is those initials.

Suppose, now, that you wish, from inside a script, to refer to this subtable. How is this to be done, when you don't know its name? Of course, I know that my subtable is called people.man; but I don't know what yours is called, so how can I write a script to give to you that can reliably refer to the corresponding subtable in your database? The solution is to use the fact that the user's initials are also stored as an entry in the database, at user.initials. On my machine, the value of user.initials is "man"; I don't know what it is on yours, but my script can find out.

But how is the script to make use of this knowledge? It cannot be right to speak of people.user.initials; this is a reference to an entry called initials in a table called user in the people table. What we want is to construct, at runtime, a database object reference, composed of people followed by the value of user.initials. This is signified by enclosing the part of the name to be calculated in square brackets, like this:

people.[user.initials]

This notation forces pre-evaluation of the expression in square brackets, and then uses that value in constructing an object reference. What goes into the square brackets can be any evaluatable UserTalk expression. To illustrate further, in this script:

s = "Hello"
workspace.[s] = "Hi"

the variable s is evaluated so that a database entry workspace.Hello is created and assigned the value "Hi". In this script:

workspace.[date.daystring(date.dayofweek(clock.now()))] = 3

three built-in Frontier verbs are called, so that a database entry workspace.Saturday (because I'm writing this on a Saturday) is created and assigned the value 3.

For certain database entries, this square-bracket syntax is actually required in order to reference them at all. That's because these database entries have names that are in some way nonstandard. A standard database entry name consists entirely of alphanumeric characters (an underscore is also permitted), beginning with a letter. It is permissible to give a database entry a name that does not conform to this rule, but then one cannot refer to it normally; for example, one could manually create an entry in the workspace table called 1997, but referring to it as:

workspace.1997

generates a syntax error. The solution is a trick: specify 1997 in the reference by using the string literal "1997" and forcing its evaluation with square brackets, as follows:

workspace.["1997"]

Because a number will be coerced to a string in this kind of expression, it is also possible in this case to say workspace.[1997], without the quotation marks.2

The same notation also handles the situation where a name contains spaces or other punctuation:

workspace.["my workspace entry"]

Also, this notation is the only way to refer to a name that is a UserTalk constant or keyword. UserTalk constants and keywords are evaluated before anything else; so if you tried to speak of an entry called if in the workspace table as workspace.if, the if would be seen as the keyword if and a syntax error would result. However, workspace.["if"] refers successfully to such an entry. This is discussed further in Chapter 9, Special Evaluation .

All we have said here of database entry names is equally true of variable names. For instance, the following script assigns the value "Fun!" to a local variable route66 by constructing the variable's name:

local (route66)
["route" + (33 + 33)] = "Fun!"

There is also another, entirely different mechanism for calculating object names: this involves constructing and dereferencing a string, which actually belongs to the technique of referring to objects not by name, but by address. This is discussed in Chapter 8, Addresses and Chapter 10, Datatypes .

Partial References

To save programmers from the clumsy, error-prone tedium of having to use a full object reference to access any item in the database, UserTalk permits the use of partial references for entries in the database. In a partial reference, the first element or elements of the name are omitted. To understand how to use partial references, you need to know the rules by which Frontier resolves a database entry name.

In what follows we pretend that there are no variables - just database entries. The way the presence of variables complicates the picture is discussed in the next chapter.

The Search Order

When Frontier sees an object reference in a UserTalk expression, it tries to interpret it by looking for the object in a series of set locations in the database, as follows:

  • 1. The database itself is called root , and so the name of every item in the database theoretically begins with root , but it is always permitted to omit this. If an object reference does begin with root, Frontier sees the object reference as not being a partial reference. If an object reference does not begin with root, Frontier initially tries to resolve it by prefixing root to it.

So, for example, a reference suites is taken to mean root.suites (which exists); a reference root.workspace is taken to mean root.workspace (which exists); a reference workspace.veeblefetzer is taken to mean root.workspace.veeblefetzer (which, I believe it is safe to assume, does not exist).

  • 2. Frontier next tries to resolve the reference by consulting the list of search paths. This list is maintained at system.misc.paths; if you look at the table, which is reproduced as Table 6-1, you can interpret it by mentally removing the initial @ from each of the values (not the names).3 Frontier runs down the list from top to bottom, prefixing each search path in turn to the object reference (also prefixing root if the name does not now begin with root).
Table 6-1 Search paths

Name

Value

path00

@people.man

path01

@system.verbs.globals

path02

@system.macintosh.globals

path03

@system.verbs.builtins

path04

@system.compiler.["kernel"].lang

path05

@system.macintosh

path06

@system.compiler.["kernel"]

path07

@system.verbs.constants

path08

@system.macintosh.constants

path09

@root.suites

path10

@system.verbs.apps

path11

@root.system

path12

@system.extensions

path13

@system.verbs.colors

So, for example, a reference long is resolved as follows. First (step 1), Frontier finds that root.long does not exist; then (step 2), it finds that root.peo-ple.[user.initials].long does not exist; then (continuing with step 2) that root.system.verbs.globals.long does exist, and that's the end of the search.

How Frontier Behaves

We now understand the order in which Frontier searches. But what is Frontier's behavior during the search? When does it decide to stop the search? What constitutes failure to resolve the reference, and what constitutes success?

Distinguish two cases, having to do with the form in which the original object reference is given (that is, what you originally said, not what variations Frontier may construct on the name during the search process). Let's call them the x form and x.y form. The x form is where you provide only a single element, such as long. The x.y form is where you provide more than one element; everything but the last element is x. So, if you say workspace.myTable.myEntry, then workspace.myTable is x, and myEntry is y.

  • 1. If the original reference is of the x form, then Frontier is happy to get a value or modify an existing value, but under no circumstances will it create a new database entry. This is logical, because you haven't sufficiently specified where a new entry would go. Therefore, if the search process constructs the name of an object that exists, the search stops: the name has been resolved. Otherwise the search fails after all the search paths are exhausted.
  • 2. If the original reference is of the x.y form, then Frontier takes the first element of x and looks in each search path for a table of that name. If it never finds one, the search fails after all the search paths are exhausted. If it does find one, the search stops,4 the name is reassembled, and the following tests are applied until one of them is satisfied:
  • a. If x.y exists, the name has been resolved.
  • b. If the whole of x does not exist, the search fails.
  • c. If x exists, and x is not a table, the search fails.
  • d. If x exists and is a table, then if the original command was to assign a value to y, y is created and the value is assigned.
  • e. If x exists and is a table, but the original command was to obtain a value from y, the search fails because y does not exist.

If the search fails, a runtime error is raised.

All of rules 2a through 2e make perfect sense given the premise in rule 2 itself; it is that premise which is, perhaps, somewhat surprising. If you were looking for the "pl" in "people", you would not stop at the first "p" and declare that there is no "pl" just because this "p" is not followed by "l". But what Frontier does is rather like this, and the results can be counterintuitive.

Suppose, for example, that we wish to know the value of system.misc. paths.path00. There is a search path to root.system ; yet if we ask for misc.paths.path00, an error is generated. Why? Because, according to rule 2, Frontier begins by searching for just misc. There is no root.misc, so Frontier starts trying the search paths in turn. Well, before Frontier ever tries the search path root.system , it tries the search path system.macintosh. This table contains a subtable called misc! The search now stops. Since system.macintosh.misc turns out not to contain a paths, the search is declared a failure (the whole of x does not exist).

Being Careful About Search Paths

Since Frontier will very happily accept a partial reference to a database entry that you ask it to alter the value of, or even delete altogether, a certain amount of caution is clearly advisable. Surprises can crop up, especially when you believe you are talking about a variable and Frontier disagrees. Hitherto, we have been rather free and easy with creation of variables: in Example 4-1 we blithely said:

s = "Hello, World!"

But suppose we had said, instead:

cr = "Hello, World!"

This would not have created a variable cr; rather, it would have changed the value of an important UserTalk constant, system.verbs.constants.cr . That's because there is a search path to system.verbs.constants, so cr already exists along the search paths. By rule 1 above, Frontier would not have created cr in the database from this partial reference of form x; but it will gladly modify an existing cr. The way to avoid this type of misunderstanding is to declare your variables, as discussed in the next chapter.

Another implication is that it is possible to subvert the search process and break scripts accidentally. Suppose you were to create an entry called long in people.[user.initials]. Meanwhile, system.verbs.globals.long is an important built-in UserTalk verb; it converts its argument to an integer. For example, long("34") is 34, turning a string into a number. But if you say long("34") now, Frontier will encounter people.[user.initials].long first in the course of its search, and will stop, never finding out about system.verbs.globals.long. Therefore, the creation of an entry people. [user.initials].long causes all scripts that use the built-in UserTalk verb long() to break.

This is a pity, because people.[user.initials] is just where one would like to put utility scripts, and one would like to put them in tables with the same name as the verbs they supplement. For example, the string verbs lack a verb for getting the substring made up of the last n characters of a string. If you write one, calling it rightN(), you might wish you could put it in a table called string in people.[user.initials], so that you can call it by saying string.rightN(). But the existence of such a table would cause the verb string() to break, and with it every script in the database that calls it - and there are many.

Fortunately, there are verbs to help you learn in advance how Frontier will resolve a name you are thinking of using. Their value, in this context, lies in the fact that when you give a partial reference to one of these verbs, Frontier (naturally) resolves it before the verb receives it.

For instance, parentOf() returns the name of the table, if any, where a named object resides. Therefore, handing to parentOf() the first element of a name you are thinking of using5 tells whether it is in use along the search paths. For instance, parentOf(long) yields "system.verbs.globals" (which is a search path, or Frontier wouldn't have found it); so long is in use along the search paths, and it would be a bad idea to say:

long = 7

or to define people.[user.initials].long.

Another useful verb is defined() , which tells whether a name has a value. For example, defined(misc.paths) yields false, so this is not going to be a successful way to refer to system.misc.paths.6

Another important device is to Command-double-click, in an edit window, a name you are thinking of using. This causes Frontier to try to jump to a database entry by that name; again, the name is resolved using the search paths, though in this case other search methods are used as well (see the script at system.misc.command2click to learn more), so if the jump succeeds, the name may be in use among the search paths.

with

It often happens that, in order to save typing and improve the readability of a script, one would like to establish a temporary search path to a table which is not listed in system.misc.paths. Suppose, for example, we wish to obtain the square root of the sine of 13. Both square roots and sines are obtained through scripts in system.extensions.trigCmd. We already have a search path to system.extensions; so we could say:

x = trigCmd.sqrt ( trigCmd.sin ( 13 ))

But it is easier to read if we resort to UserTalk's with keyword, which lets us abbreviate object references. Our code would then look like this:

with trigCmd
    sqrt (sin (13))

A with statement is followed on the same line by a reference; this is called the with's domain . When it encounters the with, Frontier attempts to resolve the domain, raising a runtime error if it cannot do so (or if it isn't a table). Having resolved the domain, Frontier proceeds to the with's indented bundle, and as it resolves each reference therein, it begins with an extra step: it looks to see whether that reference names an entry in the domain table. Only if it does not does Frontier pass on to the normal reference resolution procedure.

So, in the previous example, Frontier first tries to resolve trigCmd. This succeeds, and the resolved domain for the with is the table system.extensions.trigCmd. Then, when it comes to sqrt, Frontier looks first to see if systems.extensions.trigCmd.sqrt exists. It does; if it didn't, Frontier would then have tried to resolve sqrt in the normal way, looking for root.sqrt and then traversing the search paths.

There is essentially no penalty for using, inside a with bundle, partial references to things that don't live in the domain. If we say:

with trigCmd
    dialog.notify (sqrt (sin (13)))

the search for dialog.notify will begin by looking to see whether system.extensions.trigCmd.dialog exists. It doesn't, but this extra check only takes an instant and then we're off on the normal resolution process. You can't short-circuit the extra check anyway; even if you were to say:

with trigCmd
    root.system.verb.builtins.dialog.notify (sqrt (sin (13)))

Frontier would begin by checking for system.extensions.trigCmd.root.

On the other hand, you occasionally will need some extra specification to force Frontier to look outside the domain. For example, there is a built-in verb delete() which deletes database entries. If you were to say:

with Eudora
    « ... some stuff ...
    delete (myEntry)
    « ... some more stuff ...

there would be a problem; Eudora , which resolves to system.verbs. apps.Eudora, contains an entry called delete, which isn't the one you mean. Since the domain is checked first, you need to be more explicit:

with Eudora
    « ... some stuff ...
    system.verbs.globals.delete (myEntry)
    « ... some more stuff ...

The name you give inside the with bundle is what counts in deciding whether the original reference is of the form x or of the form x.y. Therefore, by rule 1 ("How Frontier Behaves," earlier in this chapter), a partial reference consisting of just one element in a with bundle can never cause a new database entry to be created. Saying:

with workspace
    x = 7

cannot create a new database entry, but only set an existing one. This provides a measure of security; you have not been sufficiently specific (you might mean to create workspace.x, but you might not), so Frontier is cautious.

It is possible to list more than one domain in a with, separating them by commas. This is really just a shorthand for nested withs; in other words:

with Eudora, eventInfo
    x = signature

is equivalent to saying:

with Eudora
    with eventInfo
        x = signature

Each subsequent domain is resolved by checking first to see if it is in an earlier domain. Here, for instance, after Eudora is resolved as system.verbs.apps.Eudora, the first step in resolving eventInfo is to see whether it is in that domain table. (It is.) References inside nested with s are resolved by checking each enclosing domain, starting with the innermost and working outward, before proceeding to normal resolution. So here in the case of x, Frontier looks first for system.verbs.apps.Eudora.eventInfo.x, then for system.verbs.apps.Eudora.x, and then goes on to root.x and the search paths.

There is more to know about with; see the next chapter.


1. This, of course, does not mean that a database entry, once created, lives forever: persistent does not mean permanent. You can delete a database entry manually, and the same action can be performed from within a script.

2. It is also permissible to omit the preceding dot; thus, workspace["1997"] would work just as well. But one cannot use workspace[1997] in this case, because it has another meaning. These two notations are not object references, but array operations, which are dealt with in Chapter 11, Arrays .

3. The value of the first entry in the table depends upon the user's initials, and was created when Frontier was first obtained and initialized, so mine reads @people.man, but yours probably doesn't. In any case, the path leads to the people.[user.initials] table.

4. Actually, this is not quite true. If the original reference is a verb call of the form x.y(), and if the first element of x is found but x.y does not exist, the search continues. But there are almost no circumstances where it is helpful to know this.

5. A good way to do this is in the Quick Script window. Choose Quick Script from the Open menu, type parentOf( whatever ) into the edit box, and click the Run button (or hit the Enter key). The answer comes back in the response box at the bottom.

6. A paradox arises with verb calls, because of the rule mentioned a couple of footnotes back. To illustrate: if you create a table in the people.[user.initials] table called string, then defined(string. length) returns false but string.length("hi") works anyway.


TOP UP: UserTalk Basics NEXT: The Scope of Variables and Handlers

This is Frontier: The Definitive Guide by Matt Neuburg, unabridged and unaltered from the January 1998 printing.
It deals with the freeware Frontier 4.2.3; for commercial Frontier 8, look here, and for the inexpensive Radio UserLand, look here. See my Web site for information.
Those wishing an offline or printed copy may purchase the book, which is still in print.
Copyright 1998 O'Reilly & Associates, Inc. ALL RIGHTS RESERVED. Reprinted with permission.