Hacker’s Guide to Visual FoxPro
An irreverent look at how Visual FoxPro really works. Tells you the inside scoop on every command, function, property, event and method of Visual FoxPro.
To him who looks upon the world rationally, the world in its turn presents a rational aspect. The relation is mutual.
—Georg Wilhelm Friedrich Hegel, Philosophy of History, 1832
FoxPro’s oldest heritage is its Xbase heritage. As we said in “It’s Always Been That Way,” Xbase has a long and varied history. This section explains the basic language concepts that derive from that history.
dBase was originally conceived primarily as an interactive language. Because of this, many of the older commands’ default behaviors focus on interactive results, with optional clauses available to use the command programmatically.
The nature of a relational database like FoxPro is that you need to work with many tables at once. You couldn’t get much work done if you had access to only one table at a time. Xbase handles this by providing multiple work areas, each capable of handling one table and its related indexes.
The available number of work areas has been one of the carrots manufacturers have used in the Xbase competition. In ancient Xbase history, there were two work areas. By dBase III Plus and FoxBase+, 10 work areas were provided and each table could be opened with up to seven index files. In FoxPro 2.5, the number of available work areas went up to 225. Since you’d run out of file handles long before you could open 225 different tables, you’d think that would be sufficient for anyone’s needs, even if some tables are open several times. But Visual FoxPro raised the stakes again, providing 32,767 work areas. Yes, 32K work areas! (In fact, there are 32,767 work areas per data session. More on data sessions later.) While this seems extravagant (in fact, ridiculous) at first glance, it leaves room for a lot of things going on behind the scenes that weren’t there before. Visual FoxPro’s buffering and multiple data sessions require a lot more work areas than ever before. Remember that each file still needs one or more file handles (see “Hardware and Software Recommendations”), so while you can open the same tables many times, you still can’t open 400 tables at once—not that you’d ever want to.
It’s also interesting that Visual FoxPro’s databases, a much newer concept than tables, don’t require work areas. We guess Microsoft found a better way to handle this internally. There’s no documented limit on how many databases you can open simultaneously, but databases require file handles (three, actually—one each for DBC, DCT and DCX) so there is presumably some limit, even in the most recent versions of Windows.
Work areas are numbered from 1 to 32767 and can be referenced by number, as in:
SELECT 27
Doing so is usually a bad idea, except when hacking from the Command Window. Similarly, it is poor form to refer to the first 10 work areas by the letters A–J, a historical vestige of the days when there were only 10 work areas.
Once a table is open in a work area, you should refer to that work area by the table’s alias, like this:
SELECT Customer
Before that, it doesn’t matter which work area you use. You can get to the lowest available work area by issuing:
SELECT 0
or to the highest available work area (why? we dunno, but you can) with:
SELECT SELECT(1)
To open a table in the lowest available work area, there are
two choices. As above, you can use SELECT 0
:
SELECT 0
USE MyTable
or you can incorporate the 0 in the USE command:
USE MyTable IN 0
There is one subtle (and undocumented) difference between those two sequences. The first leaves you in the work area containing the newly opened MyTable. The second leaves you in whatever work area you were in before. It opens the table, then returns to the current work area. We can’t tell you how many times we’ve fallen into this trap.
As the previous paragraph implies, you’re “in” one work area at a time. That is, one work area is always current. You can refer to fields of the table open in that work area without aliasing them. Also, many commands are scoped to the current work area.
To make a different work area current, you SELECT
it,
preferably by using the alias of the table open in that work area:
SELECT MyOtherTable
To close the table open in a work area, issue USE with no alias:
USE && closes the table in the current work area
Or specify an alias to close that one:
USE IN MyTable && closes the table opened with alias "MyTable"
This last example points out that it’s actually less and
less necessary to switch to a particular work area. In recent versions,
virtually all of the Xbase commands have an IN
clause that lets you specify to
which work area a command applies. Rather than saving the current work area,
selecting another, doing whatever you need to do, and then returning to the
original work area, you can just do the task IN the relevant work area.
Xbase (unlike SQL) is a record-based language. Just as Xbase always believes one work area is the “current” area, one record in each table is also “current.” So, in each open table, Xbase maintains a record pointer. The record pointer always points to a single record. Many commands operate on the record pointed to.
Lots of Xbase commands move the record pointer. Some are specifically designed to move the pointer, while others do it more or less as a side effect. The second type is discussed in the next section, “Scope, FOR, WHILE (and Santa) Clauses.”
Commands designed to move the record pointer can be further
divided into two groups: those that move it based on position and those that
move it based on content. GO
and SKIP
fall into the position category, while
LOCATE
, CONTINUE
and SEEK
are in the content category.
Both GO
and SKIP
are affected by an active index order. If
you’ve SET ORDER TO
something other than 0, GO TOP
and GO BOTTOM
move to the
top and bottom of the current order, respectively. (However, they can do so
pretty slowly. See “Faster than a Speeding Bullet” for hints on doing
this better.)
Only GO <n>
is absolute. It moves to the record whose
record number is <n>, regardless of any order. This, of course, makes GO <n>
nearly useless. Its only real utility is in returning the record pointer
to a previous position after a command has moved it. For example:
nHoldRec=RECNO()
COUNT FOR category="Oddities"
GO nHoldRec
Unfortunately, even that simple piece of code won’t always
work. If the record pointer is at EOF()
before the COUNT
, the GO
fails.
Instead, you have to do it like this:
nHoldRec=IIF(EOF(),0,RECNO())
COUNT FOR category="Oddities"
IF nHoldRec=0
GO BOTTOM
SKIP
ELSE
GO nHoldRec
ENDIF
Except in the situation above, stay away from referring to records by their record numbers. Record numbers are a volatile thing. If you delete some records and pack the table, the record numbers change. The real point is that you don’t need record numbers. FoxPro has plenty of other tools you can use to find a particular record.
Xbase commands operate on groups of records—a group may be
as small as one record or as large as an entire table. There are three ways to
specify which records are affected by an Xbase command. They are Scope, the FOR
clause and the WHILE
clause. All three reflect the record order-based nature of
Xbase processing.
Almost all the record processing commands that originated in
Xbase accept all three clauses. The group includes DELETE
, RECALL
, REPLACE
,
LIST
, DISPLAY
, AVERAGE
, COUNT
, SUM
, CALCULATE
, LABEL FORM
, REPORT FORM
, SCAN
and LOCATE
, as well as some others.
The next few sections describe each of the record-selection mechanisms. Then, “Combining the Clauses” talks about their interactions.
The designers of Xbase provided several ways of choosing a
group of records based on their position in the table: choosing all the records
in a table, choosing a single record, choosing several records in sequence, and
choosing all the records from the present location to the end of the table.
They grouped these four choices together and called them “scope.” You
specify them by including the appropriate clause, respectively: ALL
, RECORD n
, NEXT n
, or REST
. Note that <n> differs in each
context: It means a record number for RECORD
, but means “how many”
for NEXT
. Also, be aware that NEXT
and REST
are interpreted according to the
current index order. Commands that accept a scope clause use a default scope if
no scope term is included. Those commands that have the potential to be destructive
default to NEXT 1
. Commands that can’t do any damage to the database’s records
generally default to ALL
. For example, DELETE
and REPLACE
have a default scope
of NEXT 1
, while LIST
and REPORT
default to ALL
.
You’ll find that you use ALL
and NEXT
quite a bit, while
RECORD
and REST
seem somewhat antiquated in a set-based world. In fact, there
are some good times to use REST
, but we haven’t used RECORD
in years.
The FOR
clause allows a command to process records based on
content, regardless of the position of the record in the table.
Prior to FoxPro 2.0, this made a FOR
clause something to be
avoided at all costs. With Rushmore optimization, FOR
clauses can be very fast.
(That’s “FOR better.”) However, if a FOR
expression can’t be optimized,
it can ruin performance. (That’s “FOR worse.”)
FOR
accepts a logical expression and processes all records
for which that expression is true. This makes it just the thing for getting a
list of all customers in the U.K.:
BROWSE FOR Country = "U.K."
or for replacing all occurrences of a misspelled string with the correctly spelled version:
REPLACE LastName WITH "Quayle" FOR LastName="Quail" AND NOT IsBird
Note that giving REPLACE
a FOR
(or WHILE
) clause changes its
default scope to ALL
.
Any command including a FOR
clause may be optimizable. See
“Faster Than a Speeding Bullet” for hints on how to optimize these
commands.
WHILE
is used when records are already ordered the way you
want. It processes records starting with the current record and continues until
it encounters a record that doesn’t meet the condition.
WHILE
is handy for things like totaling all the detail
records of an invoice:
SELECT Detail
SET ORDER TO InvId
SEEK Invoice.InvId
SUM DetAmt WHILE InvId=Invoice.InvId TO InvTotal
or changing the area code for all the people with a particular phone exchange:
SELECT People
* Tag Phone is on the actual phone number field;
* area code is a separate field.
SET ORDER TO Phone
SEEK "555"
REPLACE AreaCode WITH "610" WHILE Phone="555"
Our first version of the “SEEK, then REPLACE WHILE” example ran into a nasty, subtle problem in working with ordered data. We planned to show changing the ZIP code field for everyone in a particular ZIP code (something that actually happened to Tamar a few years ago). The code looks like this:
SET ORDER TO Zip
SEEK "19117"
REPLACE Zip WITH "19027" WHILE Zip="19117"
But, using this approach, you can’t do that. With order set
to ZIP code, once you change the ZIP code field for a record, it moves to its
new position. In the example, as soon as one record gets the new ZIP code, it
moves to join the other “19027”s. Unless you’re particularly lucky,
when the record pointer moves forward, it’s no longer on a “19117”
record and the REPLACE
ends. You can run into the same problem when you SCAN
with order set.
To do the ZIP code replacement, you need to use REPLACE FOR
instead:
REPLACE Zip WITH "19027" FOR Zip="19117"
When you use WHILE
, you need to position the record pointer
at the beginning of the group of records you want to process. When you finish,
the record pointer is on the first record that doesn’t match the criteria.
Scope, FOR
and WHILE
can be combined in a single command.
The interactions are logical, but not necessarily intuitive.
First, realize that FOR
has an implied (or default) scope of
ALL
. Any command containing FOR without a scope clause or a WHILE
clause checks
all records. This means it’s not necessary to use the ALL
keyword here:
REPLACE ALL vegetable WITH "Lettuce" FOR vegetable = "Broccoli"
If no other scope is included, and there’s no WHILE
clause,
the record pointer is left at EOF()
after a FOR
clause.
WHILE
has an implied scope of REST
, but it may stop short of
the end of the file if the WHILE
condition is no longer met.
So, what happens when you combine these babies? Adding scope
to either FOR
or WHILE
means that it behaves as it usually does, but processes
records only in the specific scope. So:
BROWSE NEXT 20 FOR State = "Confusion"
displays all of the next 20 records that meet the FOR
criteria, but shows matches only from among those 20, even if fewer than 20 are
included.
Combining FOR
and WHILE
seems contradictory, but actually
it’s not. The key to understanding such a command is knowing that the WHILE
is
in charge with the FOR
just helping out. In other words, we process records
until we find one that fails the WHILE
test, but we process only those that
pass the FOR
test. For example:
SELECT Employee
SET ORDER TO GroupId
SEEK " 2"
LIST First_Name, Last_Name WHILE GroupId = SPACE(5)+"2" ;
FOR NOT EMPTY(Photo)
lists only those employees in Group 2 for whom we have a photo on file.
As we mentioned above, a few commands accept only FOR
and
not WHILE
. LOCATE
accepts both a FOR
and a WHILE
clause, but the FOR
clause is
required in that case. That is, to find Group 2 employees with photos, you can:
LOCATE FOR NOT EMPTY(Photo) WHILE GroupId = SPACE(5)+"2"
but you can’t use WHILE
to look for all the Group 2 people,
like this:
LOCATE WHILE GroupId = SPACE(5)+"2"
This behavior makes some sense. Using just WHILE
with LOCATE
isn’t very useful. You might as well use SKIP
and test the condition. WHILE
requires the records to be in order anyway, so LOCATE WHILE
is overkill.
However, we might want to search only in a specific group of records for the
first record that meets a condition. LOCATE FOR
with a WHILE
clause does that
for us.
One of the most confusing aspects of Xbase is the way string comparison works. The Xbase way is to compare strings only to the end of the right-hand string. If they match up to that point, they’re considered equal.
Given Xbase’s interactive history, it’s not really bad behavior. When you’re searching for a particular record or want to find all the records that match a given string, it’s kind of nice to not have to type out the whole string to match. But in programs, this behavior can be a disaster.
The SET EXACT
command controls string comparisons for Xbase
commands. For more than you ever wanted to know about this subject, check out
SET EXACT
in the Reference section.
“Reeling and Writhing, of course, to begin with,” the Mock Turtle replied, “and the different branches of Arithmetic—Ambition, Distraction, Uglification, and Derision.”
—Lewis Carroll, Alice’s Adventures in Wonderland, 1865
Remember seventh grade (or maybe it was sixth or eighth) when you learned a mnemonic that went something like “Please Excuse My Dear Aunt Sally?” Right about now, you’re probably scratching your head and saying, “Oh, sure, those are the Great Lakes, right?,” or “Yeah, that sounds familiar, but what’s it about?” unless, like us, you remember this kind of stuff forever, and you’re sitting there saying “Sure, parentheses, exponents, multiplication, division, addition, subtraction.” You got it: The mnemonic provides the order of precedence of numeric operators. That is, when you see:
3 + 7 * 4
you know that it’s 31, not 40, because multiplication comes before addition (except in the dictionary). On the other hand:
(3 + 7) * 4
is 40 because parentheses come before any other operators. When there are multiple operators at the same level, they’re evaluated from left to right.
In Xbase, there are a bunch of other operators besides
arithmetic operators. Comparison operators (=
, <>
, <
, >
, <=
,
>=
, and $
) are used to compare values (big surprise). Logical operators
(AND
, OR
, NOT
) are used to combine logical expressions (often comparisons).
Some of the operators used for arithmetic can also be
applied to other data types (all of them for double, integer and currency, +
and -
for characters, -
for dates and datetimes). There’s also one additional
arithmetic operator (%
, which is the same as MOD()
) that applies to numeric,
float, double, integer and currency values.
The arithmetic precedence rules have been extended to cover
the full set of operators. Arithmetic operators used on other types have the
same precedence as when they’re used with numbers. The %
has the same
precedence as multiplication and division.
The complete precedence rules for FoxPro are somewhat different from those in some other programming languages. In particular, logical operators come below arithmetic and comparison operators:
Parentheses
Exponentiation
Multiplication/Division/Modulo
Addition/Subtraction
Comparison
NOT
AND
OR
Maybe the FoxPro version of the old mnemonic should be “Please Excuse Miss Daisy Mae And Sally Combing Nits All October?” Maybe not.
A subroutine (or sub-program, as some folks call them) is a program that is called from another program, performs a task, and then returns to the calling program, which picks up where it left off. We like to think of the original routine as getting moved to the back burner until the subroutine finishes executing; then it comes back to the front burner again.
Like many programming languages, FoxPro has two kinds of subroutines: procedures and functions. The big difference in using them is that a function returns a value while a procedure doesn’t, so a function call can appear as part of an expression while a procedure call cannot.
By the way, many people refer to functions and even procedures as UDFs for “user-defined functions.” This is to distinguish them from FoxPro’s built-in functions. Historically, there have been some places where a built-in function could be used, but a UDF could not, though we can’t think of any in Visual FoxPro.
In many programming languages, there’s a distinction drawn
between procedures and functions: A subroutine is one or the other. Not so in
FoxPro. Here, any subroutine can be either a procedure or a function. In fact,
it’s not the name you give the subroutine when you define it (either PROCEDURE
MyRoutine
or FUNCTION MyRoutine
) that determines whether it’s a procedure or function;
it’s the way you call it. That is, the same subroutine can be called as a
function sometimes and as a procedure at others. Like this:
DO MyRoutine with Param1, Param2
or
? MyRoutine(Param1, Param2)
Why does this matter? Two reasons. One we already mentioned: Functions return a value. When you call a function as a procedure, the return value is thrown away. (When you call a procedure as a function, and there’s no return value, the function returns .T.)
Throughout Xbase history, when you called a routine as a function (with the parentheses at the end), you had to do something with the result. You couldn’t write:
MyRoutine()
Instead, you needed to assign the result to a variable, use it in an expression, display it, or throw it out. Here are examples of each:
MyResult = MyRoutine() && value assigned to MyResult
IF MyRoutine() > 7 ... && value tested
? MyRoutine() && value displayed
= MyRoutine() && value thrown out
The equal notation in the last example says to run the function and throw away the return value. The only reason to do it that way is if you’re running the function for what it does and not for what it returns—that is, for its side effects. (More on side effects later.)
Starting in VFP 5, you can cut to the chase and just call the function without doing anything with the result.
The other important distinction between procedures and functions has to do with parameter passing. Parameters are the way that one routine communicates with another. Think of the name of a subroutine as an instruction to do something, and the parameters as the items the instruction applies to. Let’s look at a built-in function as an example:
UPPER(LastName)
says “Apply the UPPER
function to the LastName field
and return the result.” Since UPPER()
converts whatever you give it to
uppercase, you get back something like this:
"GATES"
Sometimes the routine needs several pieces of information to
carry out its task. For example, the built-in function SUBSTR()
returns part of
a character string you pass it (that is, a substring). It accepts two or three
parameters: the string to work on, the place the substring starts, and, if you
want, the length of the substring. Here’s an example:
SUBSTR("The quick Visual FoxPro jumped over the slower dogs", ;
11, 13)
returns:
Visual FoxPro
A subroutine must indicate what parameters it expects to
receive. There are three ways to do so: by using the PARAMETERS
statement, by
using the LPARAMETERS
statement, and by listing them in parentheses in the
routine’s header line (PROCEDURE
or FUNCTION
declaration). The PARAMETERS
statement creates the parameters as private variables, while the other two
methods create them as local variables. (See “Scope It Out!” below
for the distinction between private and local variables.)
There are two ways of passing parameters: by value and by reference. The passing method indicates whether the subroutine can affect the original item passed to the routine.
If you want to be technical about it, the parameters specified in the subroutine are called “formal parameters,” while the items you pass are called “actual parameters.” The method of passing parameters determines whether the actual parameters change when the formal parameters are changed.
Got it? Neither did we, nor did our students the first few times. So let’s not be technical about it. Think of it this way, instead: When you pass by value, the items you pass are copied to the parameters listed in the subroutine. Any changes to the parameters don’t affect the items in the call. When you pass by reference, FoxPro makes an internal connection between the parameters and the items you pass, so changes to the parameters ripple back to the items you passed.
In fact, it turns out that the names of the two methods are pretty informative. When you pass by value, the value of the item is passed, not the item itself. When you pass by reference, FoxPro creates a reference to the original item. Better now?
Now here’s the sneaky part. Ordinarily, you don’t explicitly specify the parameter-passing method—it’s implied by the subroutine call. Functions pass parameters by value; procedures pass by reference. Remembering that it’s the call that determines whether it’s a procedure or a function, that’s a major implication. The same routine may behave very differently, depending how you call it.
Here’s a very simple example. We usually check parameters at the beginning of a routine to make sure they’re the right type and have realistic values. Suppose the beginning of a subroutine looks like this:
PROCEDURE ProcessDate
PARAMETER dInput
IF TYPE('dInput')<>"D"
dInput=DATE()
ENDIF
* now do whatever
Now look at these two calls to the routine:
MyDate = "Sandy"
? ProcessDate( MyDate )
or
MyDate = "Sandy"
DO ProcessDate WITH MyDate
In the first case, the variable MyDate isn’t affected by the error checking in the routine. In the second case, MyDate gets changed to today’s date (and isn’t Sandy’s mother surprised?).
Normally, this isn’t a problem because most routines are called either as procedures or as functions, not both. So let’s move on to another issue.
There’s a principle of structured programming that says functions shouldn’t have “side effects.” This means that functions should accept some parameters, do some processing and return a value—without changing anything in the environment. (It reminds us of the scouting maxim for visiting the wilderness: “Take only pictures, leave only footprints.”)
This is why parameters are passed to functions by value. (There is one case, discussed below, where it’s essential to pass by reference.) Anyway, suppose you don’t have any principles and you want to pass a parameter to a function by reference. Can you do it?
Of course you can—this is FoxPro, after all. In fact, there
are two ways to do it. One is global while the other is local. The global
approach uses a SET
command—SET UDFPARMS
. You can SET UDFPARMS TO VALUE
(the
default) or TO REFERENCE
. When you do so, all subsequent function calls use the
specified method.
The local approach is to prefix the actual parameter (the item you’re passing) with the “@” symbol. For example:
=MyFunction(@MyVariable)
calls MyFunction, passing MyVariable by reference.
On the whole, we prefer local approaches to local problems.
Remember that environmental slogan, “Think Globally, Act Locally”?
The same thing applies here. Consider the overall effects of your actions, but
take the actions over the smallest possible scope. The problem with SET
UDFPARMS
is that it can have unexpected effects. Suppose you change it to
REFERENCE
and call a function. Suppose that function in turn calls another
function, which expects its parameters to be passed by value. The inner
function may change one of its parameters (not expecting that to affect
anything outside the function), which in turn changes the results of the
function you called. What a mess.
What about the one case where you have to pass by reference? When you specify an array in a function call (with UDFPARMS set to VALUE), only the first element of the array is passed. Makes it a little hard to, say, total the array elements. To pass an entire array to a function, you must pass by reference. So, a call to our fictional array-totaling function would look like:
nTotal=SumArray(@aMyArray)
What about the flip side—suppose you want to pass by value
to a procedure. Surprisingly, there’s only one local way to do it—no equivalent
of SET UDFPARMS
in sight. All it takes is to turn the actual parameter into an
expression. This is best demonstrated by the fact that you can pass constants
to a procedure. For example, the following is a valid procedure call:
DO MyProcedure WITH 14, 29, 93, "Hike"
as long as MyProcedure expects four parameters.
But how do you turn a variable into an expression? Simple: Put parentheses around it and it becomes something that FoxPro evaluates before passing. Voila—passing by value. Here’s an example:
DO MyOtherProcedure WITH (MyVariable), (MyOtherVariable)
We’ve noticed in the last few years that more and more programmers (ourselves included) have a tendency to use only functions and not procedures. This is partly a consequence of object-oriented programming (methods, after all, feel an awful lot like functions). We’re not sure yet whether this is a good thing or a bad thing. Since many of the routines we write actually do something other than returning a value, calling them as functions certainly does violate the old rule about side effects. If nothing else, be sure to document the effects your code has on the environment.
FoxPro has allowed you to pass fewer parameters than a
routine expects for quite a long time. The actual parameters are matched to the
formal parameters (those declared with a PARAMETERS
or LPARAMETERS
statement)
one by one, and the extra formal parameters at the end get initialized to .F.
The PARAMETERS()
function (and the superior PCOUNT()
) tells you how many
parameters were actually passed, so you can give the rest useful defaults.
In Visual FoxPro things are even better, though more confusing, too. You can choose to omit actual parameters even if they’re not at the end of the list. Just put in a comma placeholder and the corresponding formal parameter gets initialized to .F.
While we welcome this change because it means parameters can
be grouped logically instead of based on the likelihood of needing them, it
also introduces new complexity. It used to be simple. Check PARAMETERS()
and
you knew that everything after that had defaulted to .F. Now, that’s not
enough. You have to check each parameter to make sure it’s got a value of the
right type. Of course, that’s a good idea in any case, since you can also pass
the wrong type anyway.
The mess you can cause with SET UDFPARMS
leads to an
observation about subroutines in general. They should make no assumptions about
the environment in which they’re called and should clean up after themselves.
This means a routine shouldn’t expect a certain variable to exist or that any
SET
commands (like SET EXACT
or SET DELETED
) will have particular values. If it
changes any of those, it should set them back to their original values before
exiting. Routines following these rules will be welcome in any programming
neighborhood.
There are at least three places you can store subroutines in FoxPro. Each routine can be stored in its own PRG file (for performance implications, see “Faster than a Speeding Bullet”), a routine can be stored with the program that uses it, or you can group a bunch of routines together into a “procedure file” (which can contain functions as well as procedures).
Prior to FoxPro 2.0, separate PRG files were too much trouble to manage. You had to remember where possibly hundreds of little programs could be found. So, most people stored subroutines used by only one program with that program, and stored more widely used subroutines in a procedure file. But there were problems with that approach, since only one procedure file could be used at a time, and there was no way to save the name of the current procedure file before switching to another.
The Project Manager changed all that. It remembered where you left a function so you didn’t have to. Suddenly, keeping every routine in a separate PRG file became feasible. Several well-respected FoxPro developers suggested directory structures to aid in this endeavor. Others offered programs to automate the process of breaking a procedure file into its component parts.
With Visual FoxPro, the waters have been muddied. The SET
PROCEDURE
command now supports an ADDITIVE
clause, so multiple procedure files
can be set. On the other hand, the Project Manager is still happy to do the
work for you. Finally, object orientation means that code is stored with data
for anything defined as an object, and the database allows some code to be
stored there as well.
Our preference is still for stand-alone PRG files for code that isn’t object method code.
Desiring this man’s art, and that man’s scope,
With what I most enjoy contented least;
Yet in these thoughts myself almost despising,
Haply I think on thee.
—William Shakespeare, Sonnet 29, l. 7
By now, it should be no surprise that the word “scope” has two distinct meanings in FoxPro. One is discussed above in “Scope, FOR, WHILE (and Santa) Clauses.” The other (which has nothing to do with the first) applies to visibility of variables.
Variables in FoxPro have three possible scopes: public, private and local. They vary as to which routines (other than the one in which the variable was created) can see and change the variable.
Public variables are exhibitionists. They’re visible in every routine in an application except any that have an identically named private or local variable. Anybody can use their value; anybody can change it.
Any variable you create in the Command Window is public.
Otherwise, to create public variables, you need to use the PUBLIC
keyword. In
older versions of FoxPro, PUBLIC
was picky—if the variable existed when you
tried to make it public, you got an error message. Although VFP no longer
complains about this, most FoxPro programmers make it a habit to write it this
way:
RELEASE PublicVar
PUBLIC PublicVar
The RELEASE
doesn’t do any harm if the variable doesn’t
exist already, but protects you if it does. Variables created this way start out
life as logical with a value of .F.
Public variables stick around until you explicitly release
them. It’s not good enough to say RELEASE ALL
, either. You’ve got to list them
out, one by one, in order to release them (unless you do it in the Command Window,
where RELEASE ALL
does release all public variables—go figure). That’s as good
a reason as any to stay away from public variables.
Actually, you should keep public variables to a minimum. As
with SET
commands, they’re often a global solution to a local problem.
Frequently, creating a private variable at the appropriate level in your
program will give you the same results.
There is one “gotcha” involving public variables. If you pass a public variable by reference (see “Pass the Parameters, Please”), that variable is hidden within the called routine. This means a reference to the variable within the called routine gives an error message. This is just another reason to avoid public variables as much as possible.
The term “private variable” is really a misnomer. These variables aren’t private. (In fact, local variables behave as the word “private” implies; see below.) Private variables are visible in the routine that creates them and in all routines lower in the calling chain, unless those routines have their own private or local variable of the same name.
Let us run that by you again. When you declare a private variable, it hides any variables of the same name that were created higher in the calling chain, but can be seen by routines lower in the calling chain.
Hmm, how about an example? Suppose you have a routine MyRoutine that creates a variable called MyVar, then calls a subroutine called MySub. MySub can see MyVar and act on it. However, if MySub declares its own private version of MyVar, MyRoutine’s MyVar is hidden and MySub acts only on its own version of MyVar. Here’s some code to demonstrate the point:
* MyRoutine.PRG
MyVar1 = 7
MyVar2 = 10
? "In MyRoutine before calling MySub", MyVar1, MyVar2
* Note that MyVar1 and MyVar2 are NOT passed as parameters
DO MySub
? "In MyRoutine after calling MySub", MyVar1, MyVar2
RETURN
PROCEDURE MySub
PRIVATE MyVar2
MyVar2 = 2
? "In MySub before doubling", MyVar1, MyVar2
MyVar1 = 2 * MyVar1
MyVar2 = 2 * MyVar2
? "In MySub after doubling", MyVar1, MyVar2
RETURN
If you run this program, you’ll see that MyVar1 ends up with a value of 14, but MyVar2 (in MyRoutine) remains 10. So, using private variables, you can hide information from a higher-level routine, but not from a lower level.
If you don’t specify otherwise, variables are created
private. Private variables are released when you return from the routine that
created them. You can explicitly release them sooner with the RELEASE
command.
You can explicitly make a variable private by declaring it
with the PRIVATE
keyword. You must do this if a variable of the same name
exists at a higher level in the calling chain.
Unlike PUBLIC
and LOCAL
, though, declaring a variable
PRIVATE
doesn’t create the variable—it just means that if you create that
variable, it will be private. This makes some sense when you notice that
PRIVATE
can take a skeleton and make all variables matching that skeleton
private—the others take only a specified list of variables. The skeleton allows
you to do things like this:
PRIVATE ALL LIKE j*
which means any variable you create beginning with J is private.
Prior to Visual FoxPro, public and private were the only choices for variables. Visual FoxPro adds local variables, which makes writing black-box code much easier.
Local variables can be seen only in the routine that creates them. No other routine can access or change them. This makes them perfect for all those bookkeeping tasks you need in a routine, like loop counters, holding part of a string, and so forth.
Here’s an example to demonstrate why local variables are much easier to work with than private variables. Suppose you have a routine that sums a specified subset of an array. That is, you pass the array, a starting element and the number of elements to sum, and the function returns the sum of those elements. Here’s the function:
* ArraySum.PRG
* Return the sum of a specified group of elements
* of an array.
LPARAMETERS aInput, nStart, nNum
* aInput = Array to Sum
* nStart = Element to start
* nNum = Number of elements to sum
LOCAL nCnt,nSum
* nCnt = Loop Counter
* nSum = running total
* Do some error checking.
* Complete checking would also make sure each array element
* summed is numeric.
IF TYPE("nStart") <> "N" OR nStart < 1
nStart = 1
ENDIF
IF TYPE("nNum") <> "N" OR nStart + nNum - 1 > ALEN(aInput)
nNum = ALEN(aInput) - nStart + 1
ENDIF
nSum = 0
FOR nCnt = nStart TO nStart + nNum - 1
nSum = nSum + aInput[nCnt]
ENDFOR
RETURN nSum
If you have a numeric array called MyArray and want to total the third through eighth elements, you’d issue this call:
? ArraySum(@MyArray,3,6)
Now suppose you need a function that totals each row of a two-dimensional array. You could write all the summing code again or you could take advantage of the code you’ve already written. The following function calls on ArraySum to total each row and just stores the result. It takes advantage of the fact that FoxPro represents all arrays internally as one-dimensional. (See “Hip, Hip, Array” in “DBF, FPT, CDX, DBC—Hike!” for more on that subject.) For good measure, the function returns the number of rows.
* SumRows.PRG
* Sum each row of an array, returning the results
* in a one-column array.
LPARAMETERS aInput, aOutput
* aInput = Array with rows to be summed
* aOutput = One-dimensional array of sums
LOCAL nCnt, nRowCnt, nColCnt
* nCnt = loop counter
* nRowCnt = number of rows in input
* nColCnt = number of columns in input
nRowCnt = ALEN(aInput, 1)
nColCnt = ALEN(aInput, 2)
* Dimension aOutput appropriately
DIMENSION aOutput(nRowCnt)
FOR nCnt = 1 TO nRowCnt
aOutput[nCnt] = ArraySum(@aInput, nColCnt * (nCnt - 1) + 1, nColCnt)
ENDFOR
RETURN nRowCnt
If you have a two-dimensional array called My2DArray, you’d get the row totals like this:
DIMENSION MyTotals[1]
= SumRows(@My2DArray, @MyTotals)
The key point here is that, in writing SumRows, we didn’t have to worry about what variables ArraySum used. Since SumRows makes all its variables local, no routine that it calls can damage its environment. Think of local variables as “a piece of the rock” for your routines.
You create local variables with the LOCAL
keyword. Local
variables are released when you complete the routine in which they were
created. You can create local arrays, as well as scalars, using LOCAL
. Newly
created local variables have logical type and a value of .F.
You may have noticed the LPARAMETERS
declaration in those
two functions. LPARAMETERS
is also an addition in Visual FoxPro. It creates
formal parameters as local variables, protecting them in the same way that
LOCAL
protects variables.
We recommend all procedures and functions use local
variables and local parameters for all internal tasks. Use private variables
only when you explicitly want lower-level routines to have access to those
variables—for example, when SCATTER
ing fields for a form (something you don’t
really need to do in VFP).