Caché Programming Orientation Guide
A Closer Look at Caché ObjectScript
[Back] [Next]
   
Server:docs2
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

Most methods and most routines are written in Caché ObjectScript. This chapter gives an overview of this language, if you intend to use it or if you need to understand code that other people have written. This chapter discusses the following topics:

A method can contain the same statements, labels, and comments as routines do. That is, all the information here about the contents of a routine also applies to the contents of a method.
If you are writing classes rather than routines, start with the section Variables.”
Routines
The following shows an example routine named demoroutine that is written in Caché ObjectScript. This example gives us an opportunity to see some common commands, operators, and functions, and to see how code is organized within a routine.
 ; this is demoroutine 
 write "Use one of the following entry points:"
 write !,"random"
 write !,"input"
 write !,"interesting"
 quit 
 
 //this procedure can be called from outside the routine
random() public {
    set rand=$RANDOM(10)+1        ; rand is an integer in the range 1-10
    write "Your random number: "_rand
    set name=$$getnumbername(rand)
    write !, "Name of this number: "_name
 }

 //this procedure can be called from outside the routine
input() public {
    read "Enter a number from 1 to 10: ", input
    set name=$$getnumbername(input)
    write !, "Name of this number: "_name
 }
 
 //this procedure can be called only from within this routine
getnumbername(number) {
    set name=$CASE(number,1:"one",2:"two",3:"three",
        4:"four",5:"five",6:"six",7:"seven",8:"eight",
        9:"nine",10:"ten",:"other")
    quit name
}

 /* write some interesting values
 this procedure can be called from outside the routine
 */
interesting() public {
    write "Today's date: "_$ZDATE($HOROLOG,3)
    write !,"Your installed version: "_$ZVERSION
    write !,"Your username: "_$USERNAME
    write !,"Your security roles: "_$ROLES  
    }
Note the following highlights:
We can execute parts of this routine in the Terminal, as a demonstration. First, the following shows a Terminal session, in which we run the routine itself. In these examples, SAMPLES> is the prompt shown in the Terminal. The text after the prompt on the same line is the entered command. The lines after that show the values that the system writes to the Terminal in response.
SAMPLES>do ^demoroutine
Use one of the following entry points:
random
input
SAMPLES>
When we run the routine, we just get help information, as you can see. It is not required to write your routines in this way, but it is common. Note that the routine includes a QUIT before the first label, to ensure that when a user invokes the routine, processing is halted before that label. This practice is also not required, but is also common.
Next, the following shows how a couple of the subroutines behave:
SAMPLES>do input^demoroutine
Enter a number from 1 to 10: -7
Name of this number: other
SAMPLES>do interesting^demoroutine
Today's date: 2010-11-30
Your installed version: Cache for Windows (x86-32) 2010.2 (Build 454U) Sun Oct 24 2010 17:14:03 EDT
Your username: UnknownUser
Your security roles: %All
SAMPLES>

Procedures, Functions, and Subroutines
Within a Caché ObjectScript routine, a label defines the starting point for one of the following units of code:
InterSystems recommends that you use procedures, because this simplifies the task of controlling the scope of variables. In existing code, however, you might also see functions and subroutines, and it is useful to be able to recognize them. The following list shows what all these forms of code look like.
procedure
label(args) scopekeyword {
    zero or more lines of code 
    QUIT returnvalue
    }
Or:
label(args) scopekeyword {
    zero or more lines of code 
    }
label is the identifier for the procedure.
args is an optional comma-separated list of arguments. Even if there are no arguments, you must include the parentheses.
The optional scopekeyword is one of the following (not case-sensitive):
returnvalue is an optional, single value to return. To return a value, you must use the QUIT command. If you do not want to return a value, you can omit the QUIT command, because the curly braces indicate the end of the procedure.
A procedure can declare variables as public variables, although this practice is not considered modern. To do this, you include a comma-separated list of variable names in square brackets immediately before scopekeyword. For details, see User-defined Code in Using Caché ObjectScript.
function
label(args) scopekeyword
    zero or more lines of code 
    QUIT optionalreturnvalue
args is an optional comma-separated list of arguments. Even if there are no arguments, you must include the parentheses.
The optional scopekeyword is either Public (the default for functions) or Private.
subroutine
label(args) scopekeyword
    zero or more lines of code 
    QUIT
args is an optional comma-separated list of arguments. If there are no arguments, the parentheses are optional.
The optional scopekeyword is either Public (the default for subroutines) or Private.
The following table summarizes the differences among routines, subroutines, functions, and procedures:
  Routine Subroutine Function Procedure
Can accept arguments no yes yes yes
Can return a value no no yes yes
Can be invoked outside the routine (by default) yes yes yes no
Variables defined in it are available after the code finishes execution yes yes yes depends on nature of the variable
The section Variable Availability and Scope,” later in this chapter, has further details on variable scope.
Note:
In casual usage, the term subroutine can mean procedure, function, or subroutine (as defined formally here).
Variables
In Caché ObjectScript, there are two kinds of variables, as categorized by how they hold data:
There are special kinds of globals not discussed here; see Caret (^) in the appendix What’s That?
Variable Names
The names of variables follow these rules:
Caché also supports a special kind of variable known as a percent variable; these are less common. The name of a percent variable starts with a percent character (%). Percent variables are special in that they are always public; that is they are visible to all code within a process. When you define percent variables, use the following rules:
For details on percent variables and variable scope, see Variable Availability and Scope,” later in this chapter.
For further details on names and for variations, see Syntax Rules in Using Caché ObjectScript. Or see Rules and Guidelines for Identifiers,” later in this book.
Variable Types
Variables in Caché ObjectScript are weakly, dynamically typed. They are dynamically typed because you do not have to declare the type for a variable, and variables can take any legal value — that is, any legal literal value or any legal Caché ObjectScript expression. They are weakly typed because usage determines how they are evaluated.
A legal literal value in Caché ObjectScript has one of the following forms:
Depending on the context, a string can be treated as a number and vice versa. Similarly, in some contexts, a value may be interpreted as a boolean (true or false) value; anything that evaluates to zero is treated as false; anything else is treated as true.
When you create classes, you can specify types for properties, for arguments to methods, and so on. The Caché class mechanisms enforce these types as you would expect. A later section of this book provides an overview of Caché data type classes.
Variable Length
There is a limit to the length of a value of a variable. If you have long strings enabled in your installation, the limit is 3,641,144 characters. If long strings are not enabled, the limit is 32,767 characters.
A later section of this book explains how to enable long string operations.
Variable Existence
You usually define a variable with the SET command. As noted earlier, when you define a global variable, that immediately affects the database.
A global variable becomes undefined only when you kill it (which means to remove it via the KILL command). This also immediately affects the database.
A local variable can become undefined in one of three ways:
To determine whether a variable is defined, you use the $DATA function. For example, the following shows a Terminal session that uses this function:
SAMPLES>write $DATA(x)
0
SAMPLES>set x=5

SAMPLES>write $DATA(x)
1
In the first step, we use $DATA to see if a variable is defined. The system displays 0, which means that the variable is not defined. Then we set the variable equal to 5 and try again. Now the function returns 1.
In this example and in previous examples, you may have noticed that it is not necessary to declare the variable in any way. The SET command is all that you need.
If you attempt to access an undefined variable, you get the <UNDEFINED> error. For example:
SAMPLES>WRITE testvar
 
WRITE testvar
^
<UNDEFINED> *testvar
Variable Availability and Scope
Caché ObjectScript supports the following program flow, which is similar (in most ways) to what other programming languages support:
  1. A user invokes a procedure, perhaps from a user interface.
  2. The procedure executes some statements and then invokes another procedure.
  3. The procedure defines local variables A, B, and C.
    Variables A, B, and C are in scope within this procedure. They are private to this procedure.
    The procedure also defines the global variable ^D.
  4. The second procedure ends, and control returns to the first procedure.
  5. The first procedure resumes execution. This procedure cannot use variables A, B, and C, which are no longer defined. It can use ^D, because that variable was immediately saved to the database.
The preceding program flow is quite common. Caché provides other options, however, of which you should be aware.
Summary of Variable Scope
Several factors control whether a variable is available outside of the unit of code that defines it. Before discussing those, it is necessary to point out the following environmental details:
The following table summarizes where variables are available:
Variable availability, broken out by kind of variable... Outside of unit of code that defines it (but in the same process) In other processes in the same namespace In other namespaces within same Caché instance
Local variable, private scope* No No No
Local variable, public scope Yes No No
Local percent variable Yes No No
Global variable (not percent) Yes Yes Not unless global mappings permit this†
Global percent variable Yes Yes Yes
*By default, variables defined in a procedure are private to the procedure, as noted before. Also, in a procedure, you can declare variables as public variables, although this practice is not preferred. See User-defined Code in Using Caché ObjectScript.
†Each namespace has default databases for specific purposes and can have mappings that give access to additional databases. Consequently, a global variable can be available to multiple namespaces, even if it is not a global percent variable. See the chapter Namespaces and Databases.”
The NEW Command
Caché provides another mechanism to enable you to control the scope of a variable: the NEW command. The argument to this command is one or more variable names, in a comma-separated list. The variables must be public variables and cannot be global variables.
This command establishes a new, limited context for the variable (which may or may not already exist). For example, consider the following routine:
 ; demonew 
 ; routine to demo NEW
 NEW var2
 set var1="abc"
 set var2="def"
 quit
After you run this routine, the variable var1 is available, and the variable var2 is not, as shown in the following example Terminal session:
SAMPLES>do ^demonew
 
SAMPLES>write var1
abc
SAMPLES>write var2
 
write var2
^
<UNDEFINED> *var2
If the variable existed before you used NEW, the variable still exists after the scope of NEW has ended, and it retains its previous value. For example, consider the following Terminal session, which uses the routine defined previously:
SAMPLES>set var2="hello world"
 
SAMPLES>do ^demonew
 
SAMPLES>write var2
hello world
Multidimensional Arrays
In Caché ObjectScript, any variable can be a Caché multidimensional array (also called an array). A multidimensional array is generally intended to hold a set of values that are related in some way. Caché ObjectScript provides commands and functions that provide convenient and fast access to the values; these are discussed in later sections.
You may or may not work directly with multidimensional arrays, depending on the system classes that you use and your own preferences. Caché provides a class-based alternative to use when you want a container for sets of related values; see Collection Classes,” later in this book.
Basics
A multidimensional array consists of any number of nodes, defined by subscripts. The following example sets several nodes of an array and then prints the contents of the array:
 set myarray(1)="value A"
 set myarray(2)="value B"
 set myarray(3)="value C"
 zwrite myarray
 
This example shows a typical array. Notes:
A multidimensional array has one reserved memory location for each defined node and no more than that. For a global, all the disk space that it uses is dynamically allocated.
Structure Variations
The preceding examples show a common form of array. Note the following possible variations:
Use Notes
For those who are learning Caché ObjectScript, a common mistake is to confuse globals and arrays. It is important to remember that any variable is either local or global, and may or may not have subscripts. The following table shows the possibilities:
    Example and Notes
Local No subscripts Set MyVar=10
Variables like this are quite common. The majority of the variables you see might be like this.
Has subscripts
A local array like this is useful when you want to pass a set of related values.
Global No subscripts Set ^MyVar="saved note"
In practice, globals usually have subscripts.
Has subscripts Set ^MyVar($USERNAME,"Preference 1")=42
Operators
This section provides an overview of the operators in Caché ObjectScript; some are familiar, and others are not.
Operator precedence in ObjectScript is strictly left-to-right; within an expression, operations are performed in the order in which they appear. You can use explicit parentheses within an expression to force certain operations to be carried out ahead of others.
Typically you use parentheses even where you do not strictly need them. It is useful to other programmers (and to yourself at a later date) to do this because it makes the intent of your code clearer.
Familiar Operators
Caché ObjectScript provides the following operators for common activities:
Unfamiliar Operators
Caché ObjectScript also includes operators that have no equivalent in some languages. The most important ones are as follows:
Commands
This section provides an overview of the commands that you are most likely to use and to see in Caché ObjectScript. These include commands that are similar to those in other languages, as well as others that have no equivalent in other languages.
The names of commands are not case-sensitive, although they are shown in running text in all upper case by convention.
Familiar Commands
Caché ObjectScript provides commands to perform familiar tasks such as the following:
Commands for Use with Multidimensional Arrays
In Caché ObjectScript, you can work with multidimensional arrays in the following ways:
Special Variables
This section introduces some Caché special variables. The names of these variables are not case-sensitive.
Some special variables provide information about the environment in which the code is running. These include the following:
Others include $JOB, $ZTIMEZONE, $IO, and $ZDEVICE.
Other variables provide information about the processing state of the code. These include $STACK, $TLEVEL, $NAMESPACE, and $ZERROR.
$SYSTEM Special Variable
The special variable $SYSTEM provides language-independent access to a large set of utility methods.
The special variable $SYSTEM is an alias for the %SYSTEM package, which contains classes that provide class methods that address a wide variety of needs. The customary way to refer to methods in %SYSTEM is to build a reference that uses the $SYSTEM variable. For example, the following command executes the SetFlags() method in the %SYSTEM.OBJ class:
 DO $SYSTEM.OBJ.SetFlags("ck")
Because names of special variables are not case sensitive (unlike names of classes and their members), the following commands are all equivalent:
 DO ##class(%SYSTEM.OBJ).SetFlags("ck")
 DO $System.OBJ.SetFlags("ck")
 DO $SYSTEM.OBJ.SetFlags("ck")
 DO $system.OBJ.SetFlags("ck")
The classes all provide the Help() method, which can print a list of available methods in the class. For example:
SAMPLES>d $system.OBJ.Help()
'Do $system.OBJ.Help(method)' will display a full description of an individual method.
 
Methods of the class: %SYSTEM.OBJ
 
CloseObjects()
     Deprecated function, to close objects let them go out of scope.
 
Compile(classes,qspec,&errorlog,recurse)
     Compile a class.
 
CompileAll(qspec,&errorlog)
     Compile all classes within this namespace
....
You can also use the name of a method as an argument to Help(). For example:
SAMPLES>d $system.OBJ.Help("Compile")
Description of the method:class Compile:%SYSTEM.OBJ
 
Compile(classes:%String="",qspec:%String="",&errorlog:%String,recurse:%Boolean=0)
Compile a class.
<p>Compiles the class <var>classes</var>, which can be a single class, a comma separated list, 
a subscripted array of class names, or include wild cards. If <var>recurse</var> is true then 
do not output the intial 'compiling' message or the compile report as this is being called inside 
another compile loop.<br>
<var>qspec</var> is a list of flags or qualifiers which can be displayed with 
'Do $system.OBJ.ShowQualifiers()'
and 'Do $system.OBJ.ShowFlags()
Locking and Concurrency Control
An important feature of any multi-process system is concurrency control, the ability to prevent different processes from changing a specific element of data at the same time, resulting in corruption. Consequently, Caché ObjectScript provides a lock management system. This section provides a brief summary.
Also see Locks, Globals, and Namespaces,” later in this book.
Basics
The basic locking mechanism is the LOCK command. The purpose of this command is to delay activity in one process until another process has signaled that it is OK to proceed.
It is important to understand that a lock does not, by itself, prevent other processes from modifying the associated data; that is, Caché does not enforce unilateral locking. Locking works only by convention: it requires that mutually competing processes all implement locking with the same lock names.
You can use the LOCK command to create locks (replacing all previous locks owed by the process), to add locks, to remove specific locks, and to remove all locks owned by the process.
For the purpose of this simple discussion, the LOCK command uses the following arguments:
The following describes a common lock scenario: Process A issues the LOCK command, and Caché attempts to create a lock. If process B already has a lock with the given lock name, process A pauses. Specifically, the LOCK command in process A does not return, and no successive lines of code can be executed. When the process B releases the lock, the LOCK command in process A finally returns and execution continues.
Caché automatically uses the LOCK command internally in many cases, such as when you work with persistent objects (discussed later in this book) or when you use certain Caché SQL commands.
The Lock Table
Caché maintains a system-wide, in-memory table that records all current locks and the processes that own them. This table — the lock table — is accessible via the Management Portal, where you can view the locks and (in rare cases, if needed) remove them. Note that any given process can own multiple locks, with different lock names (or even multiple locks with the same lock name).
When a process ends, Caché automatically releases all locks that the process owns. Thus it is not generally necessary to remove locks via the Management Portal, except in the case of an application error.
The lock table cannot exceed a fixed size, which you can specify. For information, see Monitoring Locks in the Caché Monitoring Guide. Consequently, it is possible for the lock table to fill up, such that no further locks are possible. If this occurs, Caché writes the following message to the cconsole.log file:
LOCK TABLE FULL
Filling the lock table is not generally considered to be an application error; Caché also provides a lock queue, and processes wait until there is space to add their locks to the lock table.
However, if two processes each assert an incremental lock on a variable already locked by the other process, that is a condition called deadlock and it is considered an application programming error. For details, see Avoiding Deadlock in the chapter “Lock Management” in Using Caché ObjectScript.
Locks and Arrays
When you lock an array, you can lock either the entire array or one or more nodes in the array. When you lock an array node, other processes are blocked from locking any node that is subordinate to that node. Other processes are also blocked from locking the direct ancestors of the locked node.
The following figure shows an example:
Introduction to Lock Types
When you create a lock, you specify a combination of lock type codes, which control the nature of the lock. This section discusses some of the key concepts of lock types.
Depending on the lock type, it is possible to create multiple locks with the same lock name. These locks can be owned by the same process or different processes, again depending on the lock type. The lock table displays information for all of them.
Any lock is either exclusive (the default) or shared. These types have the following significance:
In general, the purpose of an exclusive lock is to indicate that you intend to modify a value and that other processes should not attempt to read or modify that value. The purpose of a shared lock is to indicate that you intend to read a value and that other processes should not attempt to modify that value; they can, however, read the value.
Any lock is also either non-escalating (the default) or escalating. The purpose of escalating locks is to make it easier to manage large numbers of locks, which consume memory and which increase the chance of filling the lock table. You use escalating locks when you lock multiple nodes of the same array. For escalating locks, if a given process has created more than a specific number (by default, 1000) of locks on sibling nodes of a given array, Caché removes all the individual lock names and replaces them with a new lock at the parent level. For example, you might have 1000 locks of the form ^MyGlobal("sales","EU",salesdate) where salesdate represents dates. When the same process attempts to create another lock of this form (and these locks are all escalating), Caché removes all these locks and replaces them with a lock of the name ^MyGlobal("sales","EU"). The lock table maintains the lock count for this new lock. This lock count is currently 1001, but when you add additional lock names of the same form, the lock table increments the lock count for the lock name ^MyGlobal("sales","EU"). Similarly, when you remove lock names of the same form, the lock table decrements this lock count.
There are additional subtypes of locks that Caché treats in specific ways within transactions. For details on these and for more information on locks in general, see LOCK in the Caché ObjectScript Reference. For information on specifying the lock threshold (which by default is 1000), see LockThreshold in the Caché Parameter File Reference.
System Functions
This section highlights some of the most commonly used system functions in Caché ObjectScript.
The names of these functions are not case-sensitive.
The Caché class library also provides a large set of utility methods that you can use in the same way that you use functions. To find a method for a particular purpose, use the InterSystems Programming Tools Index.
See also Date and Time Values,” later in this chapter.
Value Choice
You can use the following functions to choose a value, given some input:
Existence Functions
You can use the following functions to test for the existence of a variable or of a node of a variable.
List Functions
Caché ObjectScript provides a native list format. You can use the following functions to create and work with these lists:
There are additional list functions as well.
If you use a list function with a value that is not a list, you receive the <LIST> error.
Note:
The system class %Library.List is equivalent to a list returned by $LISTBUILD. That is, when a class has a property of type %Library.List, you use the functions named here to work with that property. You can refer to this class by its short name, %List.
Caché provides other list classes that are not equivalent to a list returned by $LISTBUILD. These are useful if you prefer to work with classes. For an introduction, see Collection Classes,” later in this book.
String Functions
Caché ObjectScript also has an extensive set of functions for using strings efficiently:
Working with Multidimensional Arrays
You can use the following functions to work with a multidimensional array as a whole:
To work with an individual node in an array, you can use any of the functions described previously. In particular:
Character Values
Sometimes when you create a string, you need to include characters that cannot be typed. For these, you use $CHAR.
Given an integer, $CHAR returns the corresponding ASCII or Unicode character. Common uses:
The function $ASCII returns the ASCII value of the given character.
$ZU Functions
In existing code, you might see items like $ZU(n), $ZUTIL(n), $ZU(n,n), and so on, where n is an integer. These are the $ZU functions, which are now deprecated and are no longer documented. They are still available, but users are encouraged to replace them with methods and properties in the Caché class library that perform the equivalent actions. There is a table of replacements in Replacements for ObjectScript $ZUTIL Functions in the Caché ObjectScript Reference.
Date and Time Values
This section provides a quick overview of date and time values in Caché ObjectScript.
Local Time
To access the date and time for the current process, you use the $HOROLOG special variable. Because of this, in many Caché applications, dates and times are stored and transmitted in the format used by this variable. This format is often called $H format or $HOROLOG format.
$HOROLOG retrieves the date and time from the operating system and is thus always in the local time zone.
The Caché class library includes data type classes to represent dates in more common formats such as ODBC, and many applications use these instead of $H format.
UTC Time
Caché also provides the $ZTIMESTAMP special variable, which contains the current date and time as a Coordinated Universal Time value in $H format. This is a worldwide time and date standard; this value is very likely to differ from your local time (and date) value.
Date and Time Conversions
Caché ObjectScript includes functions for converting date and time values.
Details of the $H Format
The $H format is a pair of numbers separated by a comma. For example: 54321,12345
For additional details, including an explanation of the starting date, see $HOROLOG in the Caché ObjectScript Reference.
Using Macros and Include Files
As noted earlier, you can define macros and use them later in the same routine. More commonly, you define them in include files. An include file is a document with the extension .inc in Studio.
To define a macro, use the #define directive or other preprocessor directive. For example:
#define mymacro "hello world" 
To include an include file in a routine, use the #Include directive. For example:
#Include myincludefile
(Note that the syntax is slightly different in class definitions.)
To refer to a macro, use the following syntax:
$$$macroname
Or:
$$$macroname(arguments)
The preprocessor directives are documented in ObjectScript Macros and the Macro Preprocessor in Using Caché ObjectScript.
Note:
Both Studio and the Management Portal list the include files with the routines. For example, the Studio Workspace window shows include files within the Routines folder. Include files are not, however, actually routines because they are not executable.
Using Routines and Subroutines
To execute a routine, you use the DO command, as follows:
 do ^routinename
To execute a procedure, function, or subroutine (without accessing its return value), you use the following command:
 do label^routinename
Or:
 do label^routinename(arguments)
To execute a procedure, function, or subroutine and refer to its return value, you use an expression of the form $$label^routinename or $$label^routinename(arguments). For example:
 set myvariable=$$label^routinename(arguments)
In all cases, if the label is within the same routine, you can omit the caret and routine name. For example:
 do label
 do label(arguments)
 set myvariable=$$label(arguments)
In all cases, the arguments that you pass can be either literal values, expressions, or names of variables.
Passing Variables by Value or by Reference
When you invoke code, you can pass values of variables to that code either by value or by reference. In most cases, these variables are local variables with no subscripts, so this section discusses those first.
As with other programming languages, Caché has a memory location that contains the value of each local variable. The name of the variable acts as the address to the memory location.
When you pass a local variable with no subscripts to procedure, function, or subroutine, you pass the variable by value. This means that the system makes a copy of the value, so that the original value is not affected. To pass the memory address instead, place a period immediately before the name of the variable in the argument list. For example:
 do ^myroutine(.myarg)
To demonstrate this, consider the following procedure:
square(input) public
{
    set answer=input*input
    set input=input + 10
    quit answer
}
Suppose that you define a variable and pass it by value to this procedure:
SAMPLES>set myvariable=5
 
SAMPLES>write $$square^demobyref(myvariable)
25
SAMPLES>write myvariable
5
In contrast, suppose that you pass the variable by reference:
SAMPLES>set myvariable=5
 
SAMPLES>write $$square^demobyref(.myvariable)
25
SAMPLES>write myvariable
15
There are other variations of variables in addition to local variables with no subscripts. The following table summarizes all the possibilities:
Kind of Variable Passing by Value Passing by Reference
Local variable with no subscripts The default way in which these variables are passed Allowed
Local variable (with subscripts) Cannot be passed this way (only the top node would be passed) Required
Global variable (with or without subscripts) Required Cannot be passed this way (data for a global is not in memory)
Potential Pitfalls
The following items can confuse programmers who are new to Caché ObjectScript, particularly if those who are responsible for maintaining code written by other programmers:
For More Information
The chapters after this provide more detail on the topics covered in this chapter. This information is taken from the following books:
The Caché documentation also includes books on Caché MVBasic and Caché Basic, which this book does not discuss in detail.