Informix Unleashed

Previous chapterNext chapterContents


- 40 -

NewEra Language


by Gordon Gielis

The goal in this chapter is to investigate the features of the NewEra language and gain an appreciation of how you, the programmer, can best use these features to develop applications. NewEra is well equipped to support both structured programming and object-oriented approaches to project development and, with NewEra's rich set of database capabilities, is an ideal choice for commercial database projects.

NewEra is backward-compatible with most of the INFORMIX-4GL language, with the exception of the screen I/O statements.

Language Basics

This section covers procedural language syntax and structure. Most programming languages allow programmers to declare and assign values to variables and to divide a program into named sections. NewEra has inherited the language syntax of the highly successful INFORMIX-4GL.

Simple Data Types

Simple data types represent discrete items of data. Like most languages, NewEra supports the standard data types such as integers and character strings. Through the Data Class Library (DCL), NewEra also offers an assortment of object classes that provide equivalent functionality to most of the simple data types. Although DCL objects are not strictly simple data types, you can use them in place of simple data types. DCL objects obey the rules of object declaration and usage. I describe objects and classes later in this section. Table 40.1 lists the NewEra simple data types and their DCL equivalents.

Table 40.1. Simple data types.

Type DCL Equivalent Data Represented
SMALLINT BOOLEAN ixSmallInt Whole numbers between-32,767 and +32,767 inclusive.
INTEGER INT ixInteger Whole numbers between-2,147,483,647 and +2,147,483,647 inclusive.
SMALLFLOAT REAL ixSmallFloat A floating-point binary numberwith the precision of a C float.
FLOAT DOUBLE PRECISION ixFloat A floating-point binary number with the precision of a C double.
DECIMAL(p,s) DEC NUMERIC ixDecimal A fixed-point decimal number with precision of p and scale of s.
MONEY(p,s) ixMoney A currency amount with precision of p and scale of s.
DATE ixDate A calendar date.
DATETIME ixDateTime A point in time with a maximum precision of YYYY-MM-DD HH:MM:SS.FFFFF. A contiguous subset of this precision is permissible; for example, you can specify month to hour.
INTERVAL ixIntervalDF ixIntervalYM An interval of time. Intervals can be specified with two maximum precision ranges, either from years to months, or days to fractions of a second. A contig- uous subset of either precision range is permissible; for example, you can specify hour to second
CHARACTER(n) CHAR(n) ixString A character string of fixed length n up to a maximum of 32,767 characters.
VARCHAR(n) ixString A character string of variable length to a maximum of n characters.
CHAR(*)VARCHAR(*) ixString A character string of unspecified length.
TEXT ixText A character string of any length.
BYTE ixByte A type of binary large object that is a string of data.

Each variable in a NewEra program must be declared. Variables can be assigned an initial value when they are declared. You declare variables by using the NewEra VARIABLE statement. The following example declares a variable called p_counter, of type INTEGER, and assigns an initial value of 20:

VARIABLE  p_counter INTEGER = 20

You also can state the data type of a variable by reference to a database column. The following example shows how to declare a variable p_var with the same type as the column sams_column of table sams_table in the database called sams_database of the server named sams_server:

VARIABLE p_var LIKE sams_database@sams_server:sams_table.sams_column

The server and database identification is not required when a default database has been declared as discussed in the "Database Access" section later in this chapter. The advantage of this feature is that it requires recompilation of the program only to keep the program variable data type synchronized with the data type in the database.

User-Defined Data Types

The NewEra language includes three types of user-defined data types: records, arrays, and user-defined classes.

Records allow you to group a collection of other variables and treat the collection as a single unit, as shown in Listing 40.1. The record can consist of variables of differing types including object references. The individual items in the record are referred to as members of the record.

Listing 40.1. Declaring records.

VARIABLE  sams_record RECORD
             part_1 INTEGER,
             part_2 CHAR(*)
                      END RECORD
VARIABLE  sams_database_record RECORD LIKE sams_table.*

Individual variables within the record can be referenced using dot notation. For example, sams_record.part_1 refers to the integer in the record declared in Listing 40.1.

Arrays are ordered collections of homogeneous data types. The individual variables in the array are called elements. Each element of an array can be referenced by its position in the array starting at position 1. You also can declare arrays with multiple dimensions. The elements of an array can also be record and object data types. The following example declares an array of 10 elements, each of data type INTEGER:

VARIABLE sams_array[10] ARRAY OF INTEGER

The expression sams_array[1] references the first element of the array sams_array, whereas sams_array[10] references the last, or tenth element. The following example declares an array of 625 (5 * 5 * 5 * 5) elements, each of data type sams_database_record:

VARIABLE sams_new_array[5,5,5,5] ARRAY OF RECORD sams_database_record.*

The expression sams_new_array[1,1,1,1] references the first element--in this case, a record of sams_database_record type--in the array.

The memory used by a program for both arrays and records is allocated when the array or record is declared. The memory usage of both arrays and records is therefore fixed while the array or record is in scope. Arrays can have up to 32,767 elements for each dimension.

Using user-defined classes, you can define objects that can provide all the facility of both records and arrays. In fact, the user-defined classes may well have records or arrays, or both, as part of their internal structure. You examine classes and objects in greater detail later in this chapter.

Program Blocks

Programs can be divided into logical units called program blocks. The program blocks supported by NewEra are FUNCTION, HANDLER, REPORT, and MAIN. The start of a program block is identified by the appropriate keyword, and the end is identified by the same keyword preceded by the END keyword.

In the MAIN program block, your program begins execution. Each program has only one MAIN program block. The following example shows a MAIN program block:

01  MAIN
02      DISPLAY "Hello world"
03  END MAIN

Scope of Reference

In the preceding section, I discussed the data types available to you with NewEra and the methods by which variables are declared. The other important issue to consider when declaring a variable is the timing of variable declaration. The time at which the variable is declared determines the scope of reference of the variable and the timing of memory allocation for that variable.

NewEra supports three scopes of reference for variables of simple data types: local, global, and module. A variable declared within a function, handler function, or report has local scope of reference within that program block (functions, handlers, and reports are discussed later in this chapter). Variables declared outside any function, handler function, or report have module scope of reference within that module. In a program that consists of more than one source code module, variables with module scope of reference in one module have no scope of reference within other modules. You can declare a variable with global scope of reference by using the keyword GLOBAL when declaring the variable. Declaring a variable with global scope of reference in more than one source code module allows that one variable to be shared between modules. In a program that consists of only one source code module, there is no difference between module and global scope of reference.

The scope of reference controls the timing of any memory allocation NewEra must make for the variable. Variables of global and module scope of reference have memory allocated for them when the program first starts. Variables of local scope have memory allocated only when the function in which they are declared is executed. After execution of the function ceases, memory for variables of local scope of reference is deallocated.

Variables that represent an object data type are subject to the same scope of resolution rules as other variables; however, the memory allocation rules are different and are discussed in detail in later sections.

Assignment, Expressions, and Comparisons

NewEra supports automatic type coercion of compatible data types. For example, you can assign an integer to a decimal or an integer to a character successfully. Of course, if you try to assign a character to an integer, an error will result.

NewEra supports assignment of values to variables using the = operator, as shown here:

LET sams_counter = 1
LET my_pay_packet = ( number_hours  /  rate_per_hour ) *                   ( ( 1 - tax_rate ) / 100 )

This example sets the value of sams_counter to 1 and calculates the value of my_pay_packet, respectively.

The three examples shown in the following code block result in the same value being assigned to my_character. The third example illustrates the use of the concatenation operator ||.

LET my_character =  "now is the time for all good persons"
LET my_character =  "now is the " , "time for all good persons"
LET my_character =  "now is the " || "time for all good persons"

Variables can also be assigned value by the return of a call to a function, as shown here:

CALL my_function() RETURNING my_variable
LET my_variable = my_function()

The preceding statements are equivalent. I discuss functions in more detail later in this chapter.

Variables also can be assigned value by an SQL operation, as in the following example:

SELECT my_column
    INTO  my_variable
    FROM my_table

This example selects data from the my_column column in the my_table table and assigns it to the variable my_variable.

Record type variables can be assigned one to the other, as shown in Listing 40.2, if each of the members of the records is compatible for type conversion.

Listing 40.2. Assignment of records.

VARIABLE
    record_one RECORD
        item1 INTEGER,
        item2 INTEGER,
        item3 CHAR(10)
               END RECORD,
    record_two RECORD
        item4 INTEGER,
        item5 INTEGER,
        item6 INTEGER
               END RECORD
LET record_one.* = record_two.*                                  O.K.
LET record_one.item1 THRU item2 = record_two.item4 THRU item5    O.K.
LET record_two.item6 = record_one.item3                          FAIL

The third assignment in Listing 40.2 will fail because it attempts to assign the value of a member of data type character to a member of data type integer.

Variables of an object data type can be assigned using the = operator; however, the operation is fundamentally different in nature when applied to object data types. I discuss object assignment in detail in a later section.

The values of variables must be compared, and NewEra supports a range of relational and Boolean operators for this purpose. Relational and Boolean operators evaluate to an integer value of 1 or 0 corresponding to TRUE and FALSE, respectively. TRUE and FALSE have been declared as constants.

NewEra supports the keyword NULL, which is used to indicate an indeterminate or unknown value. Comparisons involving variables with a NULL value always evaluate to FALSE, with the exceptions of the IS NULL and IS NOT NULL operators. Table 40.2 lists the relational and Boolean operators supported by NewEra.

Table 40.2. Comparison operators for simple data types.

Operator Function
= Evaluates to TRUE if the values of the operands are equivalent.
== Equivalent to the = operator.
< Evaluates to TRUE if the left operand is less than the right operand.
<= Evaluates to TRUE if the left operand is less than or equal to the right operand.
> Evaluates to TRUE if the left operand is greater than the right operand.
>= Evaluates to TRUE if the left operand is greater than or equal to the right operand.
<> Evaluates to TRUE if the left operand is not equivalent to the right operand.
NOT Logical inverse.
AND Logical intersection.
OR Logical union.
MATCHES Allows pattern matching using the ? and * wildcards. The ? wildcard eliminates one position from the comparison. The * wildcard eliminates an unspecified number of positions from the comparison. Evaluates to TRUE if the pattern matches with the appropriate eliminations made.
NOT MATCHES Logical inverse of MATCHES.
BETWEEN <lower> AND <upper> Evaluates to TRUE if the value falls between <lower> and <upper> inclusive.
IS NULL Evaluates to TRUE if the value is null.
IS NOT NULL Evaluates to FALSE if the value is null.

Flow Control

Flow control statements allow you to control the order of execution of statements in a program. NewEra supports the flow control statements shown in Table 40.3.

Table 40.3. Flow control statements.

Statement Purpose
IF (expression) THEN statement_one ELSE statement_two END IF The IF statement is used to make decisions. If the (expression) evaluates to TRUE, then the statement_one will be executed; otherwise, statement_two will be executed. The ELSE statement is optional.
CASE WHEN (expression_one) statement_one WHEN (expresson_two) . . . . The CASE statement is used to make decisions. Each expression is evaluated in order, and if it evaluates to TRUE, it is executed. Control then passes to after the END CASE statement.
OTHERWISE statement_two END CASE The OTHERWISE statement is optional and is executed only if all the preceding expressions evaluate to FALSE. You can break out of a CASE statement at any time by using an EXIT CASE statement.
WHILE (expression) statement_one statement_two The WHILE statement is an iteration control statement that allows an unspecified number of iterations.
CONTINUE WHILE statement_three EXIT WHILE END WHILE The WHILE statement evaluates (expression) and, if TRUE, the statements are executed. The WHILE statement continues to execute until (expression) evaluates to FALSE. The CONTINUE WHILE statement allows the WHILE statement to be repeated without executing the statements after the CONTINUE WHILE. The EXIT WHILE statement causes control to be passed to after the END WHILE statement.
FOR counter = start TO end STEP number statement_one CONTINUE FOR statement_two EXIT FOR END FOR The FOR statement is an iteration control statement that allows for a specified number of iterations. The FOR statement initially assigns the value of start to counter and then executes the enclosed statements. On subsequent iterations counter is incremented by the value of number and if END FOR is less than or equal to end, the enclosed statements are executed. If counter exceeds end, then control passes to after the END FOR statement. The EXIT FOR and CONTINUE FOR statements operate in the same way as the EXIT WHILE and CONTINUE WHILE statements described previously.
GOTO label_name LABEL : label_name You also can cause control to jump to a location specified in the LABEL using the GOTO statement. Only locations within the same module are permitted.

Functions

Using functions, you can subdivide programs into more logical and manageable units. The use of functions is crucial to the implementation of structured programming techniques. Most languages allow the use of functions (sometimes called methods or procedures); however, a language that claims to support structured programming techniques should make the use of functions both easy and robust.

Features such as named arguments and default values for arguments increase the ease of use of functions. Formal prototyping of a function allows the compiler to check the validity of any calls made to a function, thereby increasing the robustness of the program. Named function calls are permitted only for formally declared functions.

NewEra supports the function features mentioned here. An example of a function prototype is shown in Listing 40.3.

Listing 40.3. Function prototype.

EXTERNAL FUNCTION GetAccountBalance
                     (
                     CustomerNumber  INTEGER,
                     AccountType  CHAR(8) :  "STANDARD"
                     )
 RETURNING MONEY(16,2)

This example declares a function called GetAccountBalance(). The function has been declared as an external function. The external keyword indicates that the source code for the function is in another module. The compiler does not complain if you include a call to this function without declaring it as in the example, but in doing so, you are forgoing allowing the compiler to check the arguments and return signature of the call to the function. This approach is obviously so undesirable as to suggest that it would be wise to include as a standard in any project that all functions used in a module are formally declared. NewEra makes this easy with the INCLUDE statement discussed later in this chapter.

NewEra allows you to declare a default value for a function argument. If you do not provide this argument, the compiler substitutes the default value into the function call at compile time.

The function in Listing 40.3 has been declared to accept two arguments: an integer called CustomerNumber and a character string called AccountType. The arguments become local variables within the source code for the function (in this case, in another module). The function can be called by explicitly naming the arguments in the function call or by calling the function with the arguments in the correct order, as Listing 40.4 illustrates. (The example assumes that the function has been formally declared with a prototype.)

Listing 40.4. Function calls.

VARIABLE AccountBalance MONEY(16,2)
VARIABLE BadDateTime DATETIME YEAR TO SECOND
CALL GetAccountBalance(100, "STANDARD") RETURNING AccountBalance
CALL GetAccountBalance(CustomerNumber : 100, AccountType : "STANDARD")
    RETURNING AccountBalance
CALL GetAccountBalance(AccountType : "STANDARD", CustomerNumber : 100)
    RETURNING AccountBalance
CALL GetAccountBalance(100) RETURNING AccountBalance
CALL GetAccountBalance("STANDARD", 100) RETURNING AccountBalance  FAIL
CALL GetAccountBalance(100,  "STANDARD") RETURNING BadDateTime    FAIL

In Listing 40.4, the first four calls are correct. In call number one, the position of each of the variables correlates to the correct argument in the function. The second call makes this explicit by naming the arguments. The third call demonstrates one of the strengths of formal prototyping in that you are relieved from calling the function with the arguments in the correct order. The fourth call is successful because the compiler knows to substitute a default of "STANDARD" for the second argument. The fifth and sixth calls fail to compile because the compiler checks the prototype of GetAccountBalance and detects that the program is trying to assign incompatible variables. If you do not declare the prototype, the compiler will not detect this defect.


TIP: A project of any significant size involves the development of several functions that can be extensively reused throughout the project. I highly recommend that the project managers devote some time to setting naming and usage standards for functions in a project.

Functions can also be included as part of the definition of an object. This aspect of functions is explained in the discussion of objects later in this chapter.

INCLUDE Statement

Prototyping of functions imposes some administrative overheads upon you, the programmer. Most commercial applications consist of several source code modules, increasing this administrative overhead dramatically. To alleviate this burden, NewEra provides you with the INCLUDE compiler directive. The INCLUDE directive allows you to name a file to be included in the source file at the line of the INCLUDE statement. In effect, the source code file becomes one large source code module combining the original file and the included file. You don't have access to this larger expanded file because it is passed directly to the next phase of compilation. The INCLUDE statement will import the included file regardless of content. It does not have to be a function declaration statement; however, the expanded file has to be syntactically correct to pass through the remaining steps of compilation.

When the compiler encounters an INCLUDE statement, it first looks for the file in the current directory and then in the directories named in the include directories' pathway. The include directories can be specified in the Application Builder, the Source Compiler, or as a command-line option to either of the compilers. See the appropriate section for details. Include files usually have a file type extension of 4gh--for example, filename.4gh.


TIP: The INCLUDE statement thus allows programmers and system architects to control the declaration of functions, variables, or even a collection of program statements to be centralized into a small number of standard files. This capability is particularly useful for standardizing issues such as error handling. The compiler looks in the include directories in the order they are specified, so care needs to be taken with naming conventions and include directory locations in the project standards. Time spent developing a consistent use of include files will yield significant efficiency gains, even on small projects.

Constants

The CONSTANT statement allows you to declare a name for a static or constant value, as in this example:

CONSTANT
   Pi   FLOAT  = 3.1415926,
   DevelopersName = "MY_NAME"

The compiler substitutes the stipulated value for every occurrence of the constant in the program. In this example, note that the second constant does not have a data type declared. The NewEra compiler assumes a data type compatible with the stipulated value. In this example, the constant DevelopersName is declared a character data type by the compiler. A compilation error occurs if you attempt to assign a new value to a constant.


TIP: Constants are commonly used to make a source code module more readable, more easily modified, and more reliable by reducing typing errors. Constants can be declared within an include file that can be used to propagate the constant throughout the project. Project managers should identify any constants applicable to either the problem space of the project or the coding standards adopted and publish these constants as an include file early in the project development cycle.

Built-In Functions

A built-in function is a function provided by the core NewEra language to perform commonly used routines or functions. NewEra also provides an extensive number of functions through the various class libraries that come standard; these functions are covered in more detail in later sections. Table 40.4 lists the most important built-in functions.

Table 40.4. Built-in functions.

Function Name Use
ARG_VAL(n) Returns the nth placed argument passed to the program.
DOWNSHIFT(char) Downshifts all characters in a string.
ERR_GET(n) Returns the Informix error text for error n.
ERRORLOG(char) Writes string char to the previously defined error file.
FGL_GETENV(char) Retrieves the value of the environment char from the operating system.
FGL_KEYVAL(char) Returns the ASCII number for the key pressed by the user. Includes such keys as Backspace, Tab, and Return.
LENGTH(char) Determines the number of characters in string char after trimming trailing spaces.
MESSAGEBOX Displays a user dialog box with various user options.
NUM_ARGS() Returns the number of arguments passed to the program.
PACKROW() "Packs" a record of simple data types into an ixRow object.
PROMPTBOX() Displays a dialog window prompting the user to enter a string.
SHOWHELP(n) Invokes the help display system displaying item n.
SQLEXIT() Terminates the connection of an application to an Informix server.
STARTLOG(char) Starts the error logging facility to error file char.
UNPACKROW "Unpacks" an ixRow object into a record of simple data types.
UPSHIFT(char) Upshifts all characters in the character string char.
TODAY Returns today's date.
MDY() Converts a numeric month, day, and year to a date.
CURRENT Returns the date and time of day from the system clock.
DAY(date) Returns an integer representing the day of the week for any given date.
MONTH(date) Returns an integer representing the month of the year for any given date.
YEAR(date) Returns an integer representing the year for any given date.
TIME Returns the current time of day from the system clock.
WEEKDAY(date) Returns an integer representing the day of the week for date.

NewEra also provides aggregate functions AVG(), COUNT(*), MAX(), MIN(), PERCENT(*), and SUM(). These can be used only in REPORT program blocks.

NewEra as an Object-Oriented Language

What makes a language object oriented? Many languages, particularly those designed for development of graphical user interfaces, use objects such as graphical widgets. Are these languages object oriented? With an heroic programming effort and a rigorous adherence to project standards, making a 3GL, such as C, behave like an object-oriented language would be possible. However, most programmers would accept that C is not an object-oriented language. Stroustrup, the designer of the C++ language, says this: If the term "object-oriented language" means anything, it must mean a language that has mechanisms that support the object-oriented style of programming well. There is an important distinction here. A language is said to support a technique if it provides facilities that make it convenient (reasonably easy, safe, and efficient) to use that style. First, I should define the object-orientation technique. Object-oriented programming is a technique that implements solutions as a collection of independent but cooperating objects. An object is a unique instance of a complex data type. An object has behavior characteristics specified by its class. Further, object-oriented programming allows objects of new classes to be created that inherit some or all of the behavior characteristics of objects of another class.

Important elements of the preceding definition are object, instance, class, and inherit. In my opinion, all these important elements must be supported by a language for that language to be object oriented. A language that uses objects but does not allow inheritance of object characteristics is not object oriented but merely object based.

In the following chapter, you will learn that NewEra provides facilities that make it easy, safe, and efficient to use objects, classes, and inheritance, and that NewEra is a fully featured object-oriented programming language.

Objects

An object is a software construct that associates data structures with the operations permissible with that data structure. Objects present a defined interface that controls the permission of external processes to read or manipulate the object's data structure or invoke the object's operations.

The process of associating data with operations is called encapsulation and is one of the most exciting features of object-oriented programming. It is important to realize that the public interface displayed by the object cannot reveal any of the internal data structure or operations.

Classes

You can think of a class as a template or declaration for an object. A class defines the structure and behavior of an object (that is, the data structure and the associated operations, or functions, of an object). Booch describes a class like this: "A class is a set of objects that share a common structure and a common behavior." The data structure and associated operations of the class are called the members of the class.

Objects and Classes with NewEra

In this section, you begin to examine how to use objects and classes with NewEra. Figure 40.1 illustrates a simple module structure of a NewEra program that uses objects and classes.

Figure 40.1.

Module structure.

You can follow these basic steps to use objects and classes in NewEra:

1. Design a class and declare it by using the CLASS statement. Usually, you declare the class in a separate source module with a file type extension of .4gh. This file is called the Class declaration file in Figure 40.1.

2. Develop the code that implements the class. This module contains the code that implements the internal workings of the objects of the class. This file is called the Class implementation file in Figure 40.1.

3. Develop the application that uses the object. The application need only concern itself with the public interface presented by the object. These files are called Consumer code files in Figure 40.1.

Both the class implementation and consumer code modules use the INCLUDE compiler directive to reference the class definition. Before I discuss the details of each of these steps, you should consider two important similarities that objects and simple data types share. Both types of variables need to have their data type (or class) declared before being used, and both objects and simple data types cannot be referenced by a program outside their scope of reference.

Class Declaration

NewEra requires that every object variable have a declared class. The structure of the class must be declared, traditionally in a class definition file as in Figure 40.1, so that the compiler can check the use of the object within the implementation or consumer code. A class declaration is achieved by the NewEra CLASS statement. The CLASS statement only declares the class; it does not create any objects, does not allocate any memory, and contains no executable statements. The class declaration contains only instructions used by the compiler. You should examine the CLASS statement in detail because it is at the core of object-oriented programming and design. Listing 40.5 declares a class called Customer.

Listing 40.5. Class declaration.

01    CLASS Customer
02
03        FUNCTION Customer
04                   (
05                   aNumber  INTEGER,
06                   aName CHAR(32) : NULL
07                   )
08
09        CONSTANT
10            Good SMALLINT = 0,
11            Bad SMALLINT = 1
12
13        VARIABLE
14            Number  INTEGER,
15            Name CHAR(32),
16            Status SMALLINT
17
18        SHARED VARIABLE
19            NumberOfObjects  INTEGER
20
21        FUNCTION GetStatus() RETURNING SMALLINT
22        FUNCTION SetBadStatus() RETURNING VOID
23        FUNCTION SetGoodStatus() RETURNING VOID
24        SHARED FUNCTION GetNextNumber() RETURNING INTEGER
25
26        EVENT DatabaseWrite(Mode SMALLINT) RETURNING BOOLEAN
27
28   END CLASS

With the exception of EVENT and SHARED keywords, most of the statements that form the class declaration in Listing 40.5 are familiar to you from structured programming techniques. This use should not surprise you, because object-oriented techniques are an extension of the information-hiding and modular-decomposition techniques already recognized as good structured programming. In this sense, the transition to object-oriented techniques is an evolutionary, not revolutionary, change. Next, I discuss each element of the class declaration in detail.

The Constructor Function

The statement on line 03 of the class definition in Listing 40.5 is a FUNCTION statement declaring a function called Customer. Note that the function has the same name as the class and that the function does not have a return signature. This special function called the constructor must be present in every class definition. This function is called when consumer code creates a new object of this class. Creating a new object of a class is called instantiation because it produces a new instance of an object of that class. Remember that the class declaration is only declaring the prototype of any functions in the class, not the actual function itself that is in the implementation file. Creation and destruction of objects within the consumer code module are discussed in detail in a later section.

Member Constants

Line 09 of Listing 40.5 declares a member constant. A member constant behaves just like a normal constant with the compiler substituting the declared value of the constant at compile time. A member constant of a class has module scope of reference and is referenced using the class name and the module scope resolution operator. Consider the following example from a consumer code module:

INCLUDE "customer.4gh"
VARIABLE p_local SMALLINT
LET p_local = Customer::Good

This code fragment assumes that the Customer class is declared in a file called customer.4gh. The variable p_local is assigned the value of 0, which is the value of the class constant.

Member Variables

Line 13 of Listing 40.5 declares three variables: Number as an integer, Name as a char(32), and Status as a small integer. The example uses only a simple data type, but the variables can be of any data type, including other classes such as the DCL classes discussed earlier. The statement on this line declares the internal data structure of the class. These variables are called the member variables of the class.

Member variables can be either normal, as are the member variables on line 13, or shared, as in line 18. The SHARED statement modifies the scope of resolution of the member variable. A normal member variable forms part of the data structure of the object. One variable is located in memory for each object instantiated. A shared member variable, on the other hand, is shared among all instances of objects of the class. Only one variable is located in memory for all objects of the class. Operations on a shared member variable by one object affect the value of the member variable for all objects.

Normal member variables of a class can be referenced using dot notation; for example, implementation code can reference the Status member variable of an object of class Customer as Customer.Status. This method is similar to the way in which the members of a record are accessed.

Shared member variables do not belong to any particular object, but instead they belong to the class as a whole. The syntax to reference a shared member variable is similar to that used to reference member constants. The NumberOfObjects member variable of the class Customer would be referenced as Customer::NumberOfObjects, as shown here:

INCLUDE "customer.4gh"
VARIABLE NumberOfObjects INTEGER
LET NumberOfObjects = Customer::NumberOfObjects

In this example, note that resolving the class scope of the shared member variable allows you to use two variables of the same name without conflict.

Member Functions

The statement on line 21 of Listing 40.5 declares a function called GetStatus(). This statement declares one of the operations permissible for objects of this class. These functions are called the member functions of the class.

You can call a normal member function by using the dot notation similar to the way a normal member variable is referenced, as shown in Listing 40.6.

Listing 40.6. Calling a member function.

INCLUDE "customer.4gh"
VARIABLE CustomerStatus SMALLINT
LET CustomerStatus = OurCustomer.GetStatus()
IF CustomerStatus = Customer::Good THEN
     ~~~ a good customer
END IF

You can modify the scope of member functions in the same way you do with member variables by using the SHARED statement. In Listing 40.5, line 24 is a member function declared as a shared member function. Thus, the function GetNextNumber() belongs to the class rather than to objects of the class. Shared member functions are called by resolving their scope to their class, like this:

INCLUDE "customer.4gh"
VARIABLE NextCustomerNumber INTEGER
LET NextCustomerNumber = Customer::GetNextNumber()

As the name suggests, you can use shared member functions for any functions that are specific to the class rather than objects of the class.

Defining Events for a Class

Line 26 of Listing 40.5 illustrates the declaration of an event called DatabaseWrite() for the Customer class. The declaration of an event is similar to the declaration of a normal member function. Events can accept arguments and have return signatures.

Events behave very much like normal member functions. Events cannot be declared as shared. I discuss events more fully in a later section.

Access Control for Class Members

I said earlier that objects declare a public interface and control access to the objects' member variables and member functions. Part of the goal of object-oriented programming is information hiding, reducing the complexity of the solution to a collection of cooperating objects. The internal workings of each object are not relevant to the solution, only the external behavior of the object. Information hiding is achieved by access control.

NewEra supports three levels of access for member variables and member functions: public, protected, and private. Access control statements modify the VARIABLE or FUNCTION statement. The following example declares the function GetStatus() as a protected function:

PROTECTED FUNCTION GetStatus() RETURNING SMALLINT

Public access allows any consumer code to reference the class member. Consumer code, therefore, can evaluate or assign value to public member variables, and that consumer code can call a public member function. The operations shown in the following example are legal from consumer code:

CALL Customer.GetStatus() RETURNING CustomerStatus
IF Customer.Status = 0 THEN
     ~~~ good customer
END IF

Protected access allows the member variables or functions to be referenced only from the implementation code of the class or a class derived from the class. I will show you how to derive a class in the "Inheritance" section. Essentially, declaring a class member as protected prevents consumer code from operating on or referencing that class member directly. Consumer code is forced to perform operations with the object only through the class members declared as PUBLIC. Thus, the public class members form the public interface of the object. The power and security that this capability gives the software developer is quite remarkable.

With private access, the member variables or functions of a class can be referenced only from the implementation code of the class. Private members cannot be referenced from the implementation code of derived classes. Private access allows you to create "black box" objects that cannot have the internal operation changed. Other programmers can create new classes by inheritance and modify some of the behavior of the class, but essential internal elements can remain inaccessible. I discuss more details about private access in the "Inheritance" section of this chapter.

The access control statements are in addition to the scope modifier statements; for example, you can declare a private shared member.

Class Implementation

In the preceding section, you declared the Customer class. The next step is to develop the class implementation code. The class implementation code actually instantiates new objects and performs the operations you have declared in the various member functions.

Listing 40.7 illustrates the class implementation code for the Customer class.

Listing 40.7. Class implementation.

01  INCLUDE "customer.4gh"    #   the class declaration (for the compiler)
02
03  VARIABLE NumberOfObjects  INTEGER  #   shared variables "instantiated"
04
05  FUNCTION Customer::Customer    # the implementation of the constructor
06                    (
07                    aNumber INTEGER,
08                    aName CHAR(32)
09                    )
10
11      LET SELF.Number = aNumber
12      LET SELF.Name = aName
13
14  END FUNCTION
15
16  FUNCTION Customer::GetStatus() RETURNING SMALLINT
17     RETURN SELF.Status
18  END FUNCTION
19
20  FUNCTION Customer::SetBadStatus() RETURNING VOID
21     LET SELF.Status = Customer::Bad
22  END FUNCTON
23
24  FUNCTION Customer::SetGoodStatus() RETURNING VOID
25     LET SELF.Status = Customer::Good
26  END FUNCTION
27
28  FUNCTION Customer::GetNextNumber() RETURNING INTEGER
29     VARIABLE NextNumber ixInteger
30      . . .  do something - maybe SQL from database to set value of NextNumber
31     RETURN NextNumber
32  END FUNCTION

Line 03 of Listing 40.7 illustrates the way in which shared member variables are initialized. As you might recall, shared member variables are created only once for each class.

Lines 05 through 14 define the constructor function. The main purpose of the constructor in this example is to initialize the internal data structure. You are not limited to data initialization; however, the constructor can perform almost any valid NewEra statement including database access statements. In the example, the member variables Number and Name are simple data types, and they are initialized by assigning the constructor arguments to them. Alternatively, the constructor can instantiate other objects (that is, call the other objects' constructor function) as part of its internal data structure.

An interesting feature is the use of the SELF qualifier to reference the object itself. Line 11 illustrates the way in which implementation code can refer to the object itself. This qualification is not strictly necessary, because the compiler would have been able to resolve the correct variable; however, it makes the code clearer.

The definition of the member functions starts on line 16. The member functions look very much like normal functions, except that the function name is qualified by the class name. The shared member function GetNextNumber() is defined on line 28. With the exception of the SHARED keyword, it is not noticeably different from any of the other member functions.

Using Objects and Classes in Consumer Code

In the two preceding sections, you declared the Customer class and defined the implementation code for the class. Now you can get to the business end of the project and see how you can use this simple class in some consumer code (that is, an application).

Listing 40.8 shows some consumer code that uses the Customer class.

Listing 40.8. Sample consumer code.

01  INCLUDE "customer.4gh"
02
03  MAIN                     #    the entry point for the program
04
05  VARIABLE OurCustomer Customer # declare a variable named OurCustomer
06                                # of class Customer
07  VARIABLE NewNumber INTEGER    # a program variable to hold the next number
08
09  LET NewNumber = Customer::GetNextNumber()  # gets the next customer
10                                             # number
11  LET OurCustomer = NEW Customer
12                        (
13                        aNumber : NewNumber  # customer number
14                        )
15
16 CALL OurCustomer.SetGoodStatus() # we set the status of the customer
17
18
19 END MAIN

You have now created the first simple application. Listing 40.8 illustrates the use of the MAIN statement on line 03; this statement indicates where program execution begins.

Variables Must Have Data Types

Each variable, including an object variable, is required to have a data type (or class). Line 05 of Listing 40.8 declares a variable named OurCustomer of class (data type) Customer. The compiler knows about the permitted public interface of this class because you have included the declaration of the class with the INCLUDE statement on line 01. You also declare an integer called NewNumber.

Object Instantiation

An object is instantiated with the NEW statement combined with calling the constructor function as illustrated in line 11 of Listing 40.8. Note that the constructor function has been called with just the aNumber argument. The aName argument has been declared with a default value, and therefore a value is not required (although in Listing 40.8, the member variable Name will be assigned the default value of NULL).

When an object is instantiated, NewEra creates two memory structures: the object and a reference variable. Figure 40.2 illustrates this concept.

Figure 40.2.

Object instantiation.

The reference variable is the variable OurCustomer in the example. It is called a reference variable because this variable is the only way of accessing or referencing the object itself. You see in a later section that an object can have more than one reference variable. If you omit line 11 of Listing 40.8, the subsequent statement on line 16 (where a member function of the object is called) fails because the object at that point does not exist. (In fact, the program would not compile because the compiler would detect that the object has not been created.) This is the major difference between declaring variables of simple data types and reference variables. After the statement on line 07, the integer variable NewNumber is available to be used throughout the program. In contrast, the reference variable OurCustomer is available only after it has been declared and instantiated (line 05 and line 11).

Now change the example as shown in Listing 40.9 to illustrate more clearly the difference between declaring and instantiating reference variables.

Listing 40.9. Declaring and instantiating reference variables.

01  INCLUDE "customer.4gh"
02
03  MAIN                       #    the entry point for the program
04
05  VARIABLE   Counter  INTEGER    # a looping variable
06  VARIABLE   CustomerArray  ARRAY[5] OF Customer   # an array of five
07                                                   # customers
08  FOR Counter = 1 TO 5       #    loop through all the elements of the array
09     LET CustomerArray[Counter] = NEW Customer
10                                      (
11                                      aNumber : Counter #  use looping variable
12                                      )                 #  as customer number
13  END FOR
14  END MAIN

In this example, you are populating an array with five elements, each of which is a variable of class Customer. The statement on line 09 creates a new Customer variable for each iteration of the FOR loop. At the end of the FOR loop, you have five quite separate instances of a Customer reference variable in the array, each of which "points" to a separate Customer object. Each of those Customer objects can be manipulated independently of any of the other Customer objects.

Class Inheritance

One of the essential features of an object-oriented language is the ability to create new classes that inherit some or all of the properties of an existing class. This feature is called inheritance. Inheritance allows you to modify and extend the capability of applications easily by incrementally improving the facilities offered by the applications' classes. New application features can be built on a solid known foundation of the existing application. Appropriately designed access control can minimize or even prohibit subsequent development from altering the internal operations of the existing classes.

The best way to demonstrate this facility is by example. You can create a new class based on the Customer class previously introduced. In this example, create a class of Customer members who are students. You call this new class StudentCustomer. The StudentCustomer class is a subclass of the Customer class. The class declaration statement is shown in Listing 40.10.

Listing 40.10. Inheritance.

01  INCLUDE "customer.4gh"
02
03  CLASS StudentCustomer DERIVED FROM Customer
04
05      FUNCTION StudentCustomer
06               (
07               aNumber INTEGER,
08               aName CHAR(32),
09               aStudentNumber INTEGER
10               aCampus CHAR(32)
11               )
12      PROTECTED VARIABLE
13          StudentNumber INTEGER,
14          Campus CHAR(32)
15
16      PUBLIC FUNCTION GetStudentNumber() RETURNING INTEGER
17      PUBLIC FUNCTION GetCampus() RETURNING CHAR(32)
18      PUBLIC FUNCTION SetBadStatus() RETURNING VOID
19  END CLASS

The compiler needs to know the declaration of the Customer class so that it can properly declare the StudentCustomer class members. You therefore include the declaration of the Customer class with the INCLUDE compiler directive on line 01.

The StudentCustomer class is declared on line 03 with the statement DERIVED FROM qualifying the class that StudentCustomer derives from. The StudentCustomer class inherits all the features of the Customer class.

The constructor of the StudentCustomer class is declared in lines 05 to 11. Note that the prototype of the constructor has changed from that of the Customer class.

Lines 12 to 14 declare two member variables: StudentNumber and Campus. These two new member variables are unique to the StudentCustomer class. StudentCustomer, however, has five member variables; the StudentCustomer class inherits the three member variables of the Customer class in addition to the two new member variables declared here. New member variables cannot have the same name as inherited member variables because the member variables will have the same scope within the new class.

Lines 16 and 17 declare two new member functions: GetStudentNumber() and GetCampus(). These member functions are in addition to the member functions inherited from the Customer class.

I said that inheritance allows the programmer to alter the behavior of the new class from that of the old class. The example illustrates how you can change the behavior of member functions. Line 18 declares a member function called SetBadStatus(); this is not a new member function because this member function was declared in the Customer class. By including another declaration in the StudentCustomer class, you are informing the compiler that you intend to alter the behavior of the member function in the new class. This method is called overriding the member function. The prototype of the overridden function must not be changed. Unlike the other member functions you inherit from the Customer class, you need to define a new implementation for the overridden member function.

To complete the inherited class, you need to define the class implementation code, as shown in Listing 40.11.

Listing 40.11. Implementation of subclass.

01  INCLUDE "studcust.4gh"  # include class declaration for the StudentCustomer
02
03  FUNCTION StudentCustomer::StudentCustomer        #    constructor
04                                (
05                                aNumber INTEGER,
06                                aName CHAR(32),
07                                aStudentNumber INTEGER,
08                                aCampus CHAR(32)
09                                ) : Customer
10                                    (
11                                    aNumber : aNumber,
12                                    aName : aName
13                                    )
14     LET SELF.StudentNumber = aStudentNumber
15     LET SELF.Campus = aCampus
16  END FUNCTION
17
18  FUNCTION StudentCustomer::GetStudentNumber() RETURNING INTEGER
19     RETURN SELF.StudentNumber
20  END FUNCTION
21  FUNCTION StudentCustomer::GetCampus() RETURNING CHAR(32)
22     RETURN SELF.Campus
23  END FUNCTION
24  FUNCTION StudentCustomer::SetBadStatus() RETURNING VOID
25     VARIABLE MailCommand CHAR(*)
26     #   send an e-mail to the student liaison officer (example command only)
27     LET MailCommand = "mail liaison_officer" || SELF.Name
28     RUN MailCommand
29
30     CALL Customer::SetBadStatus()
31  END FUNCTION

The constructor of the StudentCustomer class calls the constructor of the Customer class on line 09. Listing 40.11 also illustrates how you can override a function. Lines 24 to 31 define the function SetBadStatus(), which was also defined in the Customer class. When the member function SetBadStatus() is called for an object of StudentCustomer class, this code is executed. However, you can still call the SetBadStatus() member function of the Customer class, as demonstrated on line 30 (although you do not need to do so). This way, you can either completely override the operation of the member function or add additional steps to the existing function.

Inheritance is obviously a very powerful feature; it is used extensively by the NewEra language itself to provide the Standard Class Libraries discussed in the next chapter. If you do not derive a class from a specific class, NewEra derives the class from a class called ixObject. The declaration of this "root" object is well documented in the NewEra language reference, and members of this object provide many useful features that I discuss further in the "Object Assignment" section of this chapter. In other words, you also can define the class declaration of the Customer class as in the following example:

CLASS  Customer  DERIVED FROM ixObject

You can inherit new classes from inherited classes to as many levels as is appropriate for the project under development. Each class extends or modifies the behavior of the class it is derived from. The chain of inheritances is called the class hierarchy. Figure 40.3 illustrates a simple class hierarchy involving the Customer and StudentCustomer classes and their descendants.

Figure 40.3.

Class hierarchy.

The NewEra language, in common with other high-level object-oriented languages such as Java, does not support multiple inheritance. Multiple inheritance occurs when a class is derived directly from more than one class. This limitation eliminates some problems that arise through namespace conflicts. I discuss class hierarchy design further in a later section.

More on Events

I briefly touched on events earlier in this chapter; now I will describe events in more detail. You noted in the Class declaration section that the prototype of an event is similar to the prototype of a member function. In fact, events are sometimes called reference functions. The event is implemented (or handled) by a special function called a handler. Except for the fact that it uses the HANDLER statement instead of the FUNCTION statement, a handler is syntactically identical to a function. Why have events and handlers then? A handler can be assigned (or bound) to handle an event at runtime. This behavior is commonly called dynamic or late-binding. In contrast, the implementation code for a function is bound to the member function at compile time.

Dynamic binding allows consumer or implementation code to change the handler that handles a call to a member event during program execution. Classes that demonstrate multiple behavior are said to be demonstrating polymorphic behavior. If no handler has been assigned to an event, calls to that member event are effectively ignored.

In the following example, you make the Customer class change its behavior dynamically. The consumer code should be able to make the Customer object write the values of its member variables into a table called cust_table or to e-mail the details to another user. You use the event mechanism to set up this example.

First, you need to define the implementation code that will perform each of the different types of write. Then add the code shown in Listing 40.12 to the class implementation file of the Customer class (customer.4gl).

Listing 40.12. Handler implementation.

01  HANDLER Customer::DoDatabaseWrite() RETURNING BOOLEAN
02     INSERT INTO cust_table_one (Number, Name)
03     VALUES(SELF.Number, SELF.Name)
04     RETURN TRUE
05  END HANDLER
06
07  HANDLER Customer::DoEmailWrite() RETURNING BOOLEAN
08     VARIABLE MailCommand CHAR(*)
09     #   send an e-mail to another user; note simplified for clarity
10     LET MailCommand = "mail other_user " || SELF.Number  || " " || SELF.Name
11     RUN MailCommand
12     RETURN TRUE
13  END HANDLER

You have defined the implementation code for the handlers, which, as you can see, are very similar to function definitions. Note that the handlers must have the same prototype as the event declaration.

Now look at the example of consumer code shown in Listing 40.13 that uses the event mechanism.

Listing 40.13. Dynamic use of handlers.

01    IF p_database_write THEN
02        HANDLE OurCustomer.DatabaseWrite WITH Customer::DoDatabaseWrite
03    ELSE
04        HANDLE OurCustomer.DatabaseWrite WITH Customer::DoEmailWrite
05    END IF
06    CALL  OurCustomer.DatabaseWrite RETURNING p_database_write

In Listing 40.13, you see how you can dynamically bind different handlers to the event DatabaseWrite. If the program variable p_database_write is TRUE (1), the event DatabaseWrite is handled with Customer::DoDatabaseWrite. If p_database_write is FALSE, the event DatabaseWrite is handled with Customer::DoEmailWrite.

In a simple example such as this one, you could have just as easily solved the problem using a member function called DatabaseWrite accepting an argument that would control whether the information was written to the database or e-mailed. However, consider what would happen if you wanted to introduce a third option--perhaps a write to a printer instead of the database. If you had chosen a member function, you would have had to recode the member function, potentially introducing errors into what was a fully debugged program. With the event mechanism, you merely have to define the handler for the write to the printer and dynamically bind that handler to the event when required. A further benefit is that the developer of the original handlers does not have to release the source code for them. The existing class implementation need not be disturbed. As the size of the project increases, the complexity of providing polymorphic behavior using traditional functions and control flow statements imposes an unmanageable burden on the project.

So far, you have used the CALL statement to execute an event. A CALL statement is a synchronous execution of the event. An event executed in this manner behaves exactly like a member function. Events can be executed asynchronously by using the POST statement. Posted events are not executed immediately; instead, the event is placed in the NewEra event queue, and the handler for the event is executed the next time NewEra is waiting for user input. The consumer code that posted the event continues to execute the statements after the POST statement. Using the Customer class example, you can post the event DatabaseWrite() as shown here:

POST OurCustomer.DatabaseWrite()

Note that the POST statement does not expect any return data from the event, even though the event was declared to return a Boolean. NewEra ignores any return from posted events that, because the event handler has not yet executed, would be meaningless in any case. Posting an event is useful in performing tasks that are not on the "critical path" of program execution and can be deferred until the program is idle.

Object Assignment

You can assign simple data type variables to each other by using the = operator. How do you assign values to objects?

Before considering this issue, remember that when an object is instantiated in NewEra, a reference variable and the object are created. The reference variable points to the memory location of the object. (In this sense, it is similar to the pointers used in C++.) The important point to remember is that the variable and the object do in fact occupy two different memory locations. Subject to certain rules, NewEra allows the reference variable to be operated on independently of the object itself. NewEra does not allow you to manipulate the objects independently of the reference variables. You can manipulate the values contained within the member variables of the object only by using the member function or events of the object.

Confused? Consider an example using objects of class ixString. An ixString is a class that is provided by the Data Class Library. You first met the ixString when I discussed simple data types. It is an object that you can use to replace simple character strings. The ixString is widely used in all NewEra applications. In the following example, you declare and instantiate three ixStrings:

VARIABLE  FirstString ixString = NEW ixString("AAAAAA")
VARIABLE  SecondString ixString = NEW ixString("BBBBBB")
VARIABLE  ThirdString ixString = NEW ixString("CCCCCC")

Figure 40.4 illustrates the reference variables and objects created by the preceding statements.

Figure 40.4.

Reference variables and objects.

Now you can assign the reference variable SecondString to the reference variable FirstString, as shown in the following example:

LET FirstString = SecondString

Figure 40.5 illustrates the relationship between the reference variables and the objects after the assignment statement shown in the preceding example.

Figure 40.5.

Memory structure after assignment.

As you can see, the object marked ixString one now has no reference variables referring to it at all, but the object marked ixString two is referenced by both the FirstString and the SecondString reference variables. The ixString one object is effectively lost; it cannot be re-referenced, and therefore you cannot perform operations on it. The ixString one object will have its memory deallocated when NewEra performs garbage collection. (See the "Object Destruction" section later in this chapter.)

How can you change the value of the objects themselves? If the designer of the class declared the member variables as PUBLIC, and the member variables are simple data types, you can manipulate the member variables directly using dot (.) notation. However, most designers specify member variables as PROTECTED to prevent uncontrolled manipulation. In this case, to change the value of the objects, you must use member functions declared for that purpose. If the designer of the class has not declared any member functions to change the value, you simply cannot change the values. The ixString class has been declared with a public member function setValueStr(), which allows you to change the value of the ixString object. To change the value of object "ixString two" to "XXXX", you enter the following changes:

CALL FirstString.setValueStr("XXXX")
OR;
CALL SecondString.setValueStr("XXXX")

Because both reference variables in the preceding example refer to the same object, both statements have the same effect.

You have seen how you can use the = operator to copy one reference variable to another. NewEra allows you to copy one object to another with the COPY operator.

There is more to copying objects than to copying reference variables, however. Recall from the discussion of member variables that a member variable can be normal, meaning a simple data type or a member variable can be a reference variable. When you copy an object with a reference member variable, the reference member variable in the new object is assigned the same value as the reference variable in the old object. This means that the reference member variable in the new object points to or refers to the same underlying object as the old object.

To demonstrate this behavior, in the following example you derive a new class called IntegerString that inherits from the ixString class but adds a reference member variable of class ixInteger. An ixInteger class is another DCL class that provides integer-like properties. The class declaration of an IntegerString class is shown in Listing 40.14.

Listing 40.14. A class with reference member variables.

CLASS IntegerString DERIVED FROM ixString
    FUNCTION IntegerString
                (
                aStringValue CHAR(*),
                aIntegerValue INTEGER
                )
    PROTECTED VARIABLE IntegerValue ixInteger
    PUBLIC FUNCTION getIntegerValue() RETURNING INTEGER
    PUBLIC FUNCTION setIntegerValue(aInteger INTEGER) RETURNING VOID
END CLASS

The member variable IntegerValue is a reference member variable referring to an object of class ixInteger.

Now you can instantiate two reference variables of class IntegerString using the NEW statement, as shown here:

LET VariableOne = NEW IntegerString(aStringValue : "ONE", aIntegerValue : 1)
LET VariableTwo = NEW IntegerString(aStringValue : "TWO", aIntegerValue : 2)

The object structure shown in Figure 40.6 will result. The figure shows the two variables together with the ixInteger reference member variables.

The constructor of the IntegerString objects (which you have not shown) instantiates the IntegerValue reference member variables for each of the IntegerString objects.

Now examine what happens if you copy one of these variables to the other using the COPY statement, as in the following example:

LET VariableOne = COPY VariableTwo

Figure 40.6.

Memory structure.

The object structure shown in Figure 40.7 will result. The normal member variables are copied in the sense that new memory storage is allocated for them, but the reference member variables are only assigned one to the other.

Figure 40.7.

Objects after a shallow COPY.

Copying a reference variable to another reference variable without copying the underlying object is called shallow copying. This is the default behavior of NewEra objects. The designer of the class can change this behavior if required. You can specify two other types of copying for the reference member variables: null and deep. You do so in the class declaration. For example, you can specify deep copying for the IntegerValue reference variable in the example class, as shown in Listing 40.15.

Listing 40.15. Declaration of DEEP COPY.

CLASS IntegerString DERIVED FROM ixString
    FUNCTION IntegerString
                (
                aStringValue CHAR(*),
                aIntegerValue INTEGER DEEP COPY
                             )
    PROTECTED VARIABLE IntegerValue ixInteger
    PUBLIC FUNCTION getIntegerValue() RETURNING INTEGER
    PUBLIC FUNCTION setIntegerValue() RETURNING VOID
END CLASS

If you instantiate and copy the objects as shown here, it would result in the object structure shown in Figure 40.8.

Figure 40.8.

Objects after a DEEP COPY.

If you had specified NULL COPY, this object structure would have resulted in the structure shown in Figure 40.9.

Object Evaluation

You know that objects are accessed only through their reference variables. Objects themselves are complex data types that can contain members that themselves are reference variables. How then do you compare objects?

First, consider the operators that compare reference variables, as shown in Table 40.5.

Figure 40.9.

Objects after a NULL COPY.

Table 40.5. Reference variable comparison operators.

Operator Use
= or == Returns TRUE when two reference variables refer to the same object; returns FALSE otherwise.
!= or <> Returns TRUE when two reference variables do not refer to the same object; returns FALSE otherwise.
IS NULL Returns TRUE if the reference variable is null. In this case, the reference variable has been declared, but the reference variable (and therefore the object) has not been instantiated. Returns FALSE otherwise.
IS NOT NULL Returns TRUE if the reference variable is not null.

The operators in Table 40.5 do not compare the values held in the member variables of the object. It is not possible to compare the values held in the member variables of the objects unless the designer of the class has provided a member function specifically for this purpose. Remembering this point is important when designing your own classes.


TIP: If the consumer code in your project needs to compare the values held in the member variables of the objects you declare, you need to provide a member function specifically for this purpose.

I mentioned that all NewEra classes ultimately derive from a base class called ixObject. The ixObject class has a member function called isEqualTo. This member function does a byte comparison of the member variables of each of the objects and returns TRUE if the two objects contain the same values. Some of the standard classes that are provided with NewEra, particularly the visual objects, cannot support a byte comparison, so this member function is overridden to trigger a runtime error. (See the "Error Handling" section.) Consequently, this member function might not work for all your user-defined classes. Check the class library reference for each of the member variables you propose in your class. You can override the isEqualTo() member function yourself to provide a customized comparison. You use the isEqualTo() member function, like this:

IF  ObjectOne.isEqualTo(ObjectTwo) THEN
     #  values are equal
ELSE
    #  values are different
END IF

The values contained in an object's member variables are not the only way the object can be evaluated. Specifically, you might want to determine the following information about an object:

To determine the class of an object, you use the member function getClass(). The getClass() member function is another member function that all objects inherit from the ixObject class. The getClass() member function returns a lowercase character string containing the class name. For example, getClass() returns ixstring for an object of the ixString class.

Sometimes you might want to know if a particular object is of a class derived from another class. The ixObject class has a shared member function called isClassDerivedFromClass(), which can be used to determine derivation, as shown in Listing 40.16.

Listing 40.16. Determining class derivation.

IF ixObject::isClassDerivedFromClass(Object1.getClass(), Object2.getClass()) THEN
      #   Object1 is derived from Object2
ELSE
      #   Object1 is not derived from Object2
END IF

Objects are often used to hold simple data type information such as integers and character strings. As you have seen, NewEra provides the Data Class Library to provide such functionality. The classes in the Data Class Library are derived from a class called ixValue, which in turn is derived from the ixTypeInfo class (as well as the ixObject class higher in the hierarchy). The ixTypeInfo class provides a member function, getTypeCode(), that returns an integer value. The integer values returned by getTypeCode() correspond to constants declared in the ixTypeInfo class. You can use the getTypeCode() member function in the manner shown in Listing 40.17.

Listing 40.17. Use of getTypeCode().

CASE Object.getTypeCode()
WHEN ixTypeCode::SQLMoney
       #  Object is of  SQL money type
WHEN ixTypeCode::SQLInteger
WHEN ixTypeCode::SQLChar

Obviously, the ixTypeInfo member variables are only available to objects that have the ixTypeInfo class somewhere in their class hierarchy. The ixTypeInfo class also provides the getPrecision(), getScale(), and getLength() member functions that allow you to examine the precision, scale, and length of the data type. For example, you might want to know the number of the fractional positions (scale) of a decimal data type.

Object Destruction

You have seen how you can create a new object. How then are objects destroyed when they are no longer required? For every object that is created, NewEra records the number of reference variables that refer to the object. Periodically, NewEra examines all the objects to find any that are no longer referenced (that is, the number of times the object is referenced is zero). If an object is no longer used, NewEra automatically scavenges the memory that the object uses. This process is commonly called automatic garbage collection. You are relieved of the task of manually deallocating the memory for objects. Memory deallocation is a source of many programmer errors in languages such as C++ where it is a manual task.

When a reference variable loses scope of reference permanently, NewEra decreases the number of references for its object. If the number of references is zero, the memory for the object is deallocated.

In certain circumstances, you might want to hasten the process of memory deallocation by assigning the reference variables for an object with the value of NULL. This approach is particularly useful for memory hungry objects such as those that contain byte or text member variables.

Objects and Functions

A reference variable can be passed as an argument to a function. If a normal variable is passed to a function, the argument becomes a local variable inside the function. This process is commonly referred to as call by value. However, when a reference variable is passed to a function, the reference variable still points to the same object. (That is, the function does not make a copy of the object as discussed previously in the "Object Assignment" section.) Any operations that are performed on the object persist after the function has ceased execution. This process is commonly referred to as call by reference.

Calling by reference is useful in many operations. For example, in INFORMIX-4GL, you cannot pass an array to a function as an argument. In NewEra, however, you can declare a class that contains an array as a member variable and pass a reference variable for an object of this class to the function. The individual elements of the array are then accessible (depending on the access control rules of the class) within the function.

When a reference variable is passed to a function, the class of the reference variable is checked against the prototype of the function. The class must be either the class named in the function prototype or a class derived from the class named in the function prototype. Inside the function, the reference variable behaves as if it references an object of the class named in the function prototype. Consider the example shown in Listing 40.18 using the IntegerString class you declared previously. Remember that the IntegerString class has a member function called setIntegerValue().

Listing 40.18. Use of functions with objects.

01  MAIN
02
03  VARIABLE  aChar   CHAR(*)
04  VARIABLE  AnIntegerString IntegerString = NEW IntegerString
05                                                (
06                                                aStringValue : "A",
07                                                aIntegerValue : 1
08                                                )
09  .   .   .
10  CALL ExampleFunction( AnIxString : AnIntegerString) RETURNING AnIntegerString
11
12  CALL AnIntegerString.getValueStr() RETURNING aChar
13  .   .    .
14  END MAIN
15
16  FUNCTION ExampleFunction( AnIxString ixString) RETURNING ixString
17      CALL AnIxString.setValueStr("B")
18      CALL AnIxString.setIntegerValue(2)         #   bad will fail compile
17  END FUNCTION

Listing 40.18 calls a function named ExampleFunction on line 10. The prototype of this function declares that it should be passed an ixString. However, you passed the function a reference variable to the user-defined class IntegerString. An object can be treated as if it is an object of one of its base classes. Because the object has inherited from this base class, it can use all the member functions and variables of that base class. However, inside the function, the object can be treated only as an object of class ixString. If you attempt to access a member that belongs to the derived class IntegerString, the compiler reports an error.

This feature of inherited classes is powerful. You can treat them as objects of the base class or objects of their actual class depending on your goals. For example, you can be assured that libraries defined to work with particular classes will also work with any classes derived from those classes.

Functions that return a reference variable allow a shorthand notation that can reduce errors and increase clarity. The ExampleFunction discussed in Listing 40.18 returns an ixString reference variable. If you want to access the value of the ixString that is returned by this function, you can use the notation shown in Listing 40.19.

Listing 40.19. Function call notation.

VARIABLE StringOne ixString = NEW ixString("A")
VARIABLE StringTwo ixString
VARIABLE aChar CHAR(*)
CALL ExampleFunction(AnIxString : StringOne) RETURNING StringTwo
CALL StringTwo.getValueStr() RETURNING aChar

The variable aChar has the value you are seeking.

Alternatively, you can use the following notation:

VARIABLE StringOne ixString = NEW ixString("A")
VARIABLE aChar CHAR(*)
CALL ExampleFunction(AnIxString : StringOne).getValueStr() RETURNING aChar

Again, the aChar variable has the value you seek. NewEra evaluates the call to the function ExampleFunction(AnIxString : StringOne) and determines that it returns a reference variable to an ixString object. It knows this from the prototype of the function. NewEra then calls the getValueStr() member function for the referenced ixString object.

Asserting the Class of an Object with Casting

In the preceding section, you learned that an object reference can be treated as an object of its declared class or an object of any class it is derived from. You frequently have an object reference of a declared class that you know really refers to an object of a class derived from the declared class. Object references like this are often obtained as the return signatures of library functions.

Using the CAST operator, you can assert the actual class of the object rather than the declared class.

Consider the following scenario: In the Visual Class Library that comes standard with NewEra, you can find a number of visual object classes such as buttons and text boxes. You use these visual objects to create graphical user interfaces. Each visual object is placed within a window. It is often useful for a visual object to access its window, so each object in the Visual Class Library has a member function called getWindow(). The getWindow() function returns a reference to an ixWindow class object. (ixWindow is the NewEra window class.) In most sophisticated applications, creating a number of user-defined classes derived from the ixWindow class is common. The user-defined classes can display a company logo or standard menu, for example.

In the following example, you declare a window class called OurWindow that derives from ixWindow. The OurWindow class has a member function called DisplayLogo(). A button object called OurButton placed in this window gets a reference to the window by calling its getWindow() member function. However, the prototype of the getWindow() member function declares the class of the reference variable it returns as an ixWindow class. If you enter the statement shown in the following example, the compiler evaluates OurButton.getWindow() and determines that it returns a reference variable to an object of class ixWindow:

CALL OurButton.getWindow().DisplayLogo()

The problem is that the ixWindow class does not have a member function called DisplayLogo(). The compiler then returns an error. The casting operator overcomes this problem by allowing you to recast the object reference temporarily to a derived class. You use the CAST operator as shown here:

CALL (OurButton.getWindow() CAST OurWindow).DisplayLogo()

Here, the compiler is instructed to assert the class of OurButton.getWindow() to the OurWindow class, thus allowing you to use the DisplayLogo() member function.

The following are some important points about casting:

Object Lists

NewEra provides facilities to manage lists of objects with the ixVector class. The ixVector class is one of the really neat features of the NewEra language; it combines and surpasses the features of linked lists and arrays.

An ixVector is a one-dimensional, dynamic vector of object references. Figure 40.10 illustrates the concept.

Figure 40.10.

ixVector.

An ixVector is declared and instantiated as follows:

VARIABLE OurList ixVector = NEW ixVector()

Informix has implemented the ixVector as a class, allowing you to create your own classes inheriting some or all of the behavior of the ixVector. For example, you can implement a class that accepts only reference variables for Customer class objects.

The standard ixVector class provides member functions to help manage the list. The functions shown in Table 40.6 are the most important.

Table 40.6. Important member functions of ixVector.

Member Function Purpose
getCount() Returns the number of positions in the ixVector occupied by items (reference variable).
getSize() Returns the total number of available positions in the ixVector. Not all positions need be occupied.
Insert(pos, elem) Inserts a reference variable for elem at the position specified by pos. All positions greater than this are shuffled up one position.
Delete(pos) Deletes the reference variable at the position specified by pos. All positions greater than this are shuffled down one position.
DeleteAll() Deletes all the reference variables in the ixVector.
Get(pos) Returns the reference variable at the position specified by pos. (Note that in this case, the CAST operator is often used to assert the declared class of the reference variable.)
set(pos, elem) Assigns the reference variable at pos to elem.

The ixVector can provide multidimensional array-like behavior. The reference variables managed by the ixVector can be reference variables pointing to other ixVectors. Therefore, you can easily create an array of ixVectors to manage a custom object structure. The array structure does not have to be regular and can assume a schema as shown in Figure 40.11.

Figure 40.11.

Irregular ixVector array.

The ixVector manages memory dynamically, unlike an array, and can be passed by reference to functions.

NewEra provides two more list management classes that are derived from the ixVector: the ixRow and the ixRowArray class. The ixRow is a list of reference variables whose class has been derived from the ixValue class. Recall that the ixValue class of objects contains SQL data types only. The ixRow is used predominantly for database access. The ixRowArray class implements an array of ixRow objects. I discuss both of these classes in more detail in the "Database Access" section.

Class Hierarchy and Associations

NewEra provides three types of object associations with which you can develop your class hierarchy:

I have discussed Inheritance associations, so now you can look at the other association types.

In a Using association, one object uses the public interface of another object. When designing your class hierarchy, you must develop a procedure for mapping the calls to an objects' members by other objects. Doing so is particularly important if you're allocating responsibility for developing the classes among separate programming teams.

In the Containment association, a class uses another class as a member. You saw this association with the IntegerString example. You can use Containment associations to declare classes that combine the behaviors of two or more classes. Consider the example shown in Figure 40.12.

Figure 40.12.

Containment.

In this example, you see two declared classes, boat and airplane, which inherit from the ancestor called vehicle. Each of these classes has been given custom class members specific to its nature. What if you discover (maybe some time later) that you also have a hybrid object called a flying boat that has the behaviors of both the boat and airplane classes? You could inherit directly from the vehicle class, but then you would have to re-include the members from both the boat class and the airplane class. As well as being a lot of work, this approach introduces the possibility of inconsistencies between the old members and the re-included members.

A possible way around this dilemma is to declare one or both of the boat and airplane classes as members of the new class. These classes should be referenced in the constructor of the flying boat class if required. You might want to make these member variables protected, so you need to provide a public interface to access them. Listing 40.20 shows this pseudo-code.

Listing 40.20. Containment.

01  INCLUDE "boat.4gh"
02  INCLUDE "airplane.4gh"
03
04  CLASS FlyingBoat
05
06     FUNCTION FlyingBoat
07              (
08              aBoat  Boat,
09              aAirplane  Airplane
10              )
11
12     PROTECTED VARIABLE aBoat  Boat
13     PROTECTED VARIABLE aAirplane  Airplane
14
15     PUBLIC FUNCTION GetBoat() RETURNING Boat
16     PUBLIC FUNCTION GetAirplane() RETURNING Airplane
17
18  END CLASS

This compound class can then demonstrate the behavior of both the boat and airplane classes. To access a boat member, you call the GetBoat() member function and then the required boat member. For example, you would use CALL MyFlyingBoat.GetBoat().ABoatMemberFunction() (assuming the ABoatMemberFunction() has been declared PUBLIC). This sort of arrangement is good for imitating multiple inheritance hierarchies.

Implicit Application Object

NewEra provides an implicit application object defined by the class ixApp. The ixApp class contains shared member functions that provide application and system-level functions. For example, the ixApp class provides the shared member function ixApp::setCursor(), which sets the appearance of the screen cursor. All the members of the ixApp class are shared, so you don't need to instantiate an ixApp object.

Error Handling

NewEra provides a number of mechanisms to handle errors. Three main classes of errors and warnings with NewEra are categorized as follows:

The NewEra language supports a GLOBAL variable called STATUS that is set to the return status of the last operation.

NewEra Code Error

NewEra code errors are either fatal or non-fatal. A fatal error cannot be ignored and always causes termination of your program (meaning that a fatal error cannot be trapped). An example of a fatal error is -1319: The NewEra program has run out of run-time data memory space. NewEra usually displays a message dialog window advising you of a fatal error.

Non-fatal errors (and warnings) can be further divided into four groups: SQL, Screen I/O, Validation, and Expression. I deal with SQL errors in the next section.

Screen and I/O errors and warnings occur when NewEra attempts to access a system resource, such as an operating system file, and is not successful.

Validation errors and warnings could occur when the program tries to assert the type of a variable against the data type stored in the syscolval table of an Informix database. This table allows you to include discrete values for a database column. In the following example, the codes for the U.S. states are included in the syscolval table entry for state.state_code. To pass the VALIDATE statement on line 03, the value of p_state_code must be one of the state codes.

01  VARIABLE p_state_code LIKE state.state_code
02
03  VALIDATE p_state_code LIKE state.state_code

Expression errors and warnings might occur when an expression used in the program violates a language rule. A good example is a situation in which a program attempts to divide by zero.

The WHENEVER Directive

NewEra allows you to deal with code errors with the WHENEVER compiler directive. The WHENEVER directive must occur within a program block. The WHENEVER directive is followed by two keywords; the first indicates the runtime condition you're trapping and the action to take. The WHENEVER directive has the syntax shown in the following example:

WHENEVER <run-time condition> <action>

Table 40.7 summarizes the options for runtime conditions.

Table 40.7. WHENEVER runtime conditions.

Directive Runtime Condition Purpose
WHENEVER ERROR Traps any errors except Expression errors.
ANY ERROR Traps all errors including Expression errors.
WARNING Traps all warnings.
Using the WHENEVER directive, you also can specify an action to take if the program encounters the specified runtime condition. The actions shown in Table 40.8 are supported.

Table 40.8. WHENEVER action options.

Option Use
CONTINUE The program is to continue operation.
STOP The program is to terminate upon error.
CALL function() The program should call function() when an error is encountered.
GOTO label The program should go to the label named label.

Error Logging

NewEra also supports logging of errors with the ERRORLOG() built-in function. The ERRORLOG() function writes the error message to an operating system file. The date and time of the error are also written. To use the ERRORLOG() function, you must have already specified the operating system file. You do so with the STARTLOG(filename) function. STARTLOG() creates the file if it does not already exist. Obviously, STARTLOG() requires that you have operating system permission to write to the file specified.

Warnings are not automatically logged.

SQL Errors

Embedded SQL errors are handled with the WHENEVER directive in a similar fashion to NewEra code errors. You can specify other runtime conditions reflecting the different nature of SQL operations. Table 40.9 lists the SQL runtime conditions you can trap.

Table 40.9. SQL WHENEVER runtime conditions.

Runtime Condition Use
NOT FOUND Used to trap an SQL operation that does not find any rows in the database or a cursor that has reached the last row.
SQLERROR Used to trap SQL errors that have occurred in the database.
SQLWARNING Used to trap warnings issued from the database.

When you're using ODBC, errors are trapped automatically by the ODBC interface classes. The ODBC interface classes provide member functions that allow you to determine the nature of the error or warning.

NewEra also supports the SQLCA global record. The SQLCA stores information about the status of the last SQL operation. You can have only one SQLCA global record. If you're using ODBC, the SQLCA record applies to the implicit connection. NewEra also supplies an object-based equivalent called the ixSQLCA class. See the section on this class later in the chapter for details.

For database operations, the SQLCA.SQLCODE member is equivalent to the STATUS global variable.


TIP: Always using the SQLCA.SQLCODE member to check database operations rather than the STATUS is a good practice because it is a more consistent approach.

Object Errors

The standard member functions of an object from the Informix Standard Classes are external to the NewEra language. The member functions can be written in C++ or even assembler language. Therefore, you can use NewEra error handling for errors within these member functions. NewEra provides an event-based solution for handling runtime errors from these member functions.

An error within a standard member function of the Informix Standard Classes calls an event named rtError(). This event has a default handler function called ixApp::uponRtError(). The ixApp::uponRtError() calls ixApp::showRtError(), which displays a dialog window with error information. You can provide an alternative handler for this event to perform additional tasks such as logging the error message to an operating system file.

If you declare a custom class and implement the member functions in NewEra, you can still use the error-handling facilities discussed previously.


TIP: In a large client/server environment, consider locating the error log file on a shared network hard disk or partitioning the error-logging object onto a central server. This way, you can monitor only one error file instead of one on each PC. Of course, you still will need to handle the occasional network error.

Database Access

The NewEra language is database-centric, providing a rich set of facilities for database access. The choice of database access technique is dictated by the size of and flexibility required of the application.

NewEra provides the Connectivity Class Library, which provides an object-oriented, vendor-independent mechanism for accessing databases. The Connectivity Class Library has two versions: CCL/Informix and CCL/ODBC. The two versions are very similar, with the exception that CCL/ODBC extends the functionality provided in CCL/Informix to enable you to examine meta-data about the database. Using the Connectivity Class Library, you can connect to one or more databases simultaneously.

The Connectivity Class Library consists of three classes: the ixSQLCA class, the ixSQLConnect class, and the ixSQLStmt class.

The ixSQLCA class declares an object that provides the same facilities as the SQLCA record discussed in the preceding section. You can examine the value of members of this object to determine the status of any database operations. You can create an ixSQLCA object for each database connection.

Objects of class ixSQLConnect provide members that allow you to connect to and disconnect from databases. Objects of class ixSQLConnect also allow you to manage transactions, isolation options, and cancellation options.

Objects of the ixSQLStmt class provide members that allow you to manage SQL statements and cursors. You can prepare and execute SQL statements, declare named cursors, and execute most Database Definition statements.

Embedded SQL

NewEra allows you to embed SQL commands directly into the language. You can use the SELECT, UPDATE, DELETE, INSERT, and EXECUTE commands to manipulate the database directly from your application. NewEra also supports embedded Database Definition Language statements such as CREATE TABLE.

Embedded SQL is undoubtedly the easiest database access method. It is also very familiar to INFORMIX-4GL programmers. Unfortunately, you cannot access ODBC data sources using embedded SQL.

NewEra's embedded SQL also supports scroll cursors.

The Database Connection Object: ixSQLConnect

Objects of the ixSQLConnect class provide capabilities that allow you to connect to databases, set database options, and manage transactions. The CCL/ODBC version also allows you to obtain table and column information from the database (meta-data). The ixSQLConnect class declaration includes several constants corresponding to data types and occurrences in the ODBC standard.

Connecting to a Database

You create an ixSQLConnect object in the usual way for an object, as you see in the following example:

INCLUDE "ixconno.4gh"
VARIABLE MyConnection ixSQLConnect
LET MyConnection = NEW ixSQLConnect()

The connection object is created in the preceding example, but it has not yet been connected to a database. To do so, you must call the connect() member function. The code fragment in Listing 40.21 shows a call to the connect member function.

Listing 40.21. Database connect.

01  CALL MyConnection.connect
02                    (
03                    SourceName : "MY_DATABASE",
04                    UserId : "MY_NAME",
05                    authorization : "PASSWORD"
06                    )
07  IF MyConnection.getODBCErrorCode() != ixSQLConnect::SQL_Success THEN
08        #   an error connecting
09  END IF

Here, the SourceName indicates the ODBC data source that you want to connect to. (ODBC data sources are usually databases but not always.) You must establish the data source by using the ODBC manager (in the ODBC Manager for Microsoft Windows) or an Informix database connection available through I-Net. The UserId and authorization parameters are optional and are required only if the data source requires them.

The connect() member function has no return. You check for errors by calling the getODBCErrorCode() member function to check for errors. Line 07 of Listing 40.21 demonstrates how you check for connection errors.

Connecting to an ODBC data source is sometimes a little more complicated than in the preceding example. The ODBC standard caters to many different types of databases. Some of these databases have different connection requirements. The ODBC driver manager allows you to interrogate the data sources available and the connection requirements for each of these drivers. The ixSQLConnect class supports this capability with two member functions: browseConnect() and driverConnect().

The syntax of the browseConnect() member function is browseConnect(connStrIn CHAR(*)) RETURNING CHAR(*). The connStrIn element must contain the data source name in the ODBC format DSN=data source name. The function loads the ODBC driver for this data source (previously set by the ODBC manager) and interrogates the driver for the information it requires. BrowseConnect() returns a string that contains login attributes. For example, it might return DB=MY_DATABASE; UID=?; PWD=*?. The question mark indicates that the attribute has an unknown value; an asterisk followed by a question mark indicates an optional unknown value. Most of the ODBC driver developers have used similar labels for login attributes. The browseConnect() member function allows you to create a customized login dialog window for a data source.

The syntax of the driverConnect() member function is

driverConnect(window ixWindow, connStrIn CHAR(*), ÂdriverCompletion INTEGER) RETURNING CHAR(*)

Using this function, you can request a connection to a data source. If the connStrIn string does not contain sufficient information for the ODBC driver manager to make the data source connection, the ODBC driver manager displays a dialog window to request the missing login attribute. The driverCompletion parameter controls the manner in which the ODBC driver manager operates the login window.

Implicit Connection

When a NewEra program begins execution, NewEra automatically creates an ixSQLConnect object. This process is called the implicit connection. The ixSQLConnect class has a shared member function called getImplicitConnection() that returns a reference to this connection. The implicit connection is only instantiated automatically; you must still attempt to connect it to a data source.

Setting Database Options

NewEra allows you to control the behavior of any of the database connections you have made. You control the behavior by setting database options through the setConnectOption() member function of the connection. The prototype of this member is

setConnectOption(option SMALLINT, param ixValue) RETURNING VOID

You call this member function, passing it an option number and an ixValue containing a valid value for the option. The eight options shown in Table 40.10 are supported.

Table 40.10. ODBC connection options.

Option Use
SQL_Access_Mode Allows you to make the database connection either READ/WRITE or READ ONLY. The valid values are defined by two class constants:

ixSQLConnect::SQL_Mode_Read_Write

ixSQLConnect::SQL_Mode_Read_Only
SQL_Autocommit Allows you to specify that database operations performed on this connection automatically commit. Valid values are

1 = On

2 = Off (default)
SQL_Txn_Isolation Specifies the isolation level you want for this database connection. Valid values are

ixSQLConnect::SQL_Txn_Uncommitted

ixSQLConnect::SQL_Txn_Committed

ixSQLConnect::SQL_Txn_Repeatable_Read

ixSQLConnect::SQL_Serializable

ixSQLConnect::SQL_Versioning
SQL_Login_Timeout Specifies the number of seconds you will allow for a login request to succeed. The default is 15. A value of zero indicates an indefinite wait.
SQL_Opt_Trace ODBC allows you to write certain trace information to a log file. Valid values are

1 = On

2 = Off (default)
SQL_Opt_Tracefile The log file for an ODBC trace. Defaults to sql.log.
SQL_Translate_DLL Specifies the name of the DLL that contains character translation functions.
SQL_Translate_Option Specifies the current translation option. Valid values are determined by the developer of the translation DLL.

You set a database option after you instantiate a connection object and successfully connect the object to a data source. You can change database options at any time during your program. The ixSQLConnect class provides a member function, getConnectOption(), that allows you to determine the currently selected options for a database connection.

Transaction Management

In the "Setting Database Options" section of this chapter, you learned how to set some default transaction- and isolation-level behaviors. The ixSQLConnect class also enables you to manage explicit transactions. Transactions are managed with the transact() member function. The prototype of this member function is

transact(mode SMALLINT : ixSQLConnect::SQL_Commit) RETURNING VOID

The mode can be one of two values: ixSQLConnect::SQL_Commit or ixSQLConnect::SQL_Rollback, which commit and roll back the transaction, respectively. You don't need to explicitly declare the beginning of a transaction because a transaction is declared after each call to transact(), as shown in Listing 40.22.

Listing 40.22. A connection example.

01   VARIABLE  MyConnection ixSQLConnect = NEW ixSQLConnect()
02   VARIABLE MyStatement ixSQLStmt
03
04   CALL MyConnection.connect("my_database")
05   LET MyStatement = NEW ixSQLStmt(MyConnection)   #  see next section
06                                 # for ixSQLStmt discussion
07   CALL MyStatement.execDirect("DELETE FROM my_table")
08
09   IF MyStatement.getODBCErrorCode() < ixSQLConnect::SQL_Success THEN
10       CALL MyConnection.transact(mode : ixSQLConnect::SQL_Rollback
11   ELSE
12       CALL MyConnection.transact(mode : ixSQLConnect::SQL_Commit)
13   END IF
14
15    #  we are automatically back in a transaction


WARNING: Do not issue BEGIN WORK, COMMIT WORK, or ROLLBACK WORK statements using the execute() or execDirect() member functions of ixSQLStmt. If you're explicitly managing transactions, you must use transact().

Disconnecting from a Data Source

You can disconnect from a data source by using the disconnect() member function. This function does not destroy the ixSQLConnect object; this object is subject to the normal referencing rules of objects. Any ixSQLStmt objects you have created using this connection will become invalid (because they no longer have a database connection).


WARNING: Disconnecting from a data source rolls back any pending transactions.

Canceling an SQL Operation

Using the CCL/Informix version, you can cancel SQL operations. You can also set cancellation options so that all SQL operations that exceed a predefined time limit are canceled. A modal dialog window is displayed when an SQL operation is canceled, and you can control the text displayed in this window. The member functions involved are cancel(), getAutoCancel(), getCancelMode(), getCancelText(), getCancelTimeout(), isCancelAllowed(), setAutoCancel(), setCancelMode(), setCancelText(), and setCancelTimeout().


TIP: Always set a maximum time limit on an SQL operation. You can declare a class of connection objects that have the project default time-out set by their constructor.

Meta-data

Using the CCL/ODBC, you can interrogate the data source to determine the names of tables and names and data types of columns. The ixSQLConnect class offers a number of member functions for this purpose. Table 40.11 summarizes the major meta-data functions and their uses.

Table 40.11. Meta-data functions.

Member Function Use
tables Allows you to determine the names of all the tables in the data source. You can search for tables by table name, table owner, and table type. The function supports character string matching. This function returns an ixSQLStmt object that is already prepared and executed. This ixSQLStmt object allows you to fetch rows of data describing the tables (see the "Using ixSQLStmt" section for specific details on fetching data rows).
columns Allows you to determine the name and data type of columns in the data source. You can search for columns similar to the way you search for tables. Similarly, this function returns an ixSQLStmt object that you can use to fetch the column information.
getTypeInfo Allows you to determine the data type information supported by the data source. You pass an argument to this function specifying the ODBC data type of interest. Class constants have been declared for the ODBC data types. This function returns an ixSQLStmt object that allows you to fetch the information.
getInfo Along with getTypeInfo, provides information about the getFunctions ODBC facilities supported by the ODBC driver.

Error Detection

The ixSQLConnect class provides the SQLError() member function that returns error and warning information about the last database operation. You pass to the SQLError() function the ixSQLStmt object that performed the last operation. SQLError() returns the ODBC SQLState, the native error code from the database server, and the native error message from the database server.

The SQL Statement Object: ixSQLStmt

Using the ixSQLStmt class, you can perform database operations. Objects of the ixSQLStmt class are used to replace embedded SQL commands. Remember that they are objects and subject to the same declaration, instantiation, and scope rules as other objects.

Using ixSQLStmt

The constructor of the ixSQLStmt class has the following prototype: ixSQLStmt(conn : ixSQLConnect : NULL). You cannot use an ixSQLStmt object without a valid connection. However, the constructor of the ixSQLStmt allows a NULL value. This device merely accesses the SQLError() member function of the ixSQLConnect object. (Recall that the SQLError() member function requires an ixSQLStmt object.)

Listing 40.23 demonstrates the instantiation and use of an ixSQLStmt object.

Listing 40.23. ixSQLStmt example.

00  INCLUDE SYSTEM "ixstring.4gh"
01  INCLUDE SYSTEM "ixstmto.4gh"       #   declaration file ixSQLstmt
02  INCLUDE SYSTEM "ixconno.4gh"       #   declaration file ixSQLConnect
03  INCLUDE SYSTEM "ixrow.4gh"         #    declaration file ixRow
04  INCLUDE SYSTEM "ixrowar.4gh"       #    declaration file ixRowArray
05
06  VARIABLE stmtSelect ixSQLStmt = NEW ixSQLStmt(conn : ixSQLConnect::getImplicitConnection())
07  VARIABLE rwData ixRow
08  VARIABLE rarData ixRowArray
09  VARIABLE SQLState, NativeMessage CHAR(*)
10  VARIABLE Counter INTEGER = 0
11  VARIABLE ErrorCode INTEGER
12
13  CALL stmtSelect.Prepare(stmt : "SELECT * FROM my_table WHERE my_col = ? ")
14  IF stmtSelect.getODBCErrorCode() != ixSQLStmt::SQL_Success THEN
15         --   an error occurred preparing the statement
16  END IF
17
18  LET rwData = stmtSelect.allocateRow() #   instantiates ixRow with correct number of
19                                          #   values
20  LET rarData = NEW ixRowArray(rowSchema : rwData) #  creates an ixRowArray with rows like the
21                                                       #   ixRow
22
23  CALL STMTselect.setParam(n : 1, val : NEW ixString("MY_MATCHING_VALUE"))
24
25  CALL stmtSelect.execute()
26  IF stmtSelect.getODBCErrorCode() != ixSQLStmt::SQL_Success THEN
27         --   an error occurred executing the statement
28         CALL ixSQLConnect::getImplicitConnection().SQLError(stmtSelect)
29         RETURNING SQLState, ErrorCode, NativeMessage
30  END IF
31
32  WHILE Counter < 100 #    we shall have a maximum of 100 rows fetched
33      CALL stmtSelect.fetchInto(oldRow : rwData)
34      IF stmtSelect.getODBCErrorCode() != ixSQLStmt::SQL_Success THEN
35          EXIT WHILE
36      ELSE
37          LET Counter = Counter + 1
38          IF rarData.insertRow(theRow : COPY rwData) = 0 THEN
39              -- error inserting into row array
40          END IF
41      END IF
42  END WHILE

Listing 40.23 instantiates an ixSQLStmt object and then executes an SQL SELECT statement. The ixSQLStmt object fetches data into an ixRow, which you then use to create an ixRowArray.

I touched on ixRows and ixRowArrays in the section on ixVectors. An ixRow is a class that inherits from the ixVector class. The ixRow accepts only ixValue references. (An ixValue object equates to SQL data types.) The ixRowArray is a class derived from the ixVector class, which accepts only references to ixRow objects. All the ixRows in an ixRowArray must have the same rowSchema. The ixRowArray is therefore similar to a standard two-dimensional array of SQL data types. (The ixRowArray has a number of other facilities that make it very useful.)

Listing 40.23, therefore, uses an ixSQLStmt to fetch data and populate an ixRowArray.

Lines 00 to 04 declare the classes to be used in the example. The ixSQLStmt object is instantiated on line 06. Note that you're using the implicit connection in this example. Lines 06 to 11 declare the variables to be used, including the ixRow and ixRowArray.

In line 13, you call the member function prepare(). This function accepts a CHAR(*), which is the SQL operation you want to execute. You prepare an SQL statement with a placeholder denoted by the question mark. The SQL statement is checked for validity against the database. It is possible that you have made an error in the SQL. Therefore, you must check whether the prepare statement was successful. You do so by calling the getODBCErrorCode() member function. This member function returns a smallint for which valid constants have been declared in the ixSQLStmt class. Table 40.12 lists the return values.

Table 40.12. ODBC error codes.

SQL_Success The database operation executed successfully.
SQL_Success_With_Info The database operation executed successfully but with information.
SQL_No_Data_Found No data matches the WHERE criteria entered.
SQL_Error The database operation failed.
SQL_Invalid_Handle The operation failed because of an internal ODBC error.
SQL_Still_Executing An asynchronous operation is still executing.
SQL_Need_Data The driver requires parameter data values.

On line 18, you call the allocateRow() member function that instantiates an ixRow object with the correct number and type of ixValue objects to receive the result of the SQL operation. On line 20, you instantiate an ixRowArray object. The constructor of the ixRowArray accepts an ixRow, and this schema is used for all ixRows in the ixRowArray.

Line 23 illustrates how you set parameter values for any placeholders. In the example, you substitute the string "MY_MATCHING_VALUE" for the "?" in the SQL statement. The SQL statement would then read

"SELECT * FROM my_table WHERE my_col = "MY_MATCHING_VALUE""

You can reset the parameters and reuse the ixSQLStmt object without preparing the SQL statement again (as in line 13) if required.

Line 25 executes the SQL statement. If your SQL statement does not return any data, all you need to do is check the status of the database operation. However, this example fetches some data, and you begin to do this on line 33 by calling the fetchInto() function. Because you anticipate multiple rows, you place the fetch into a WHILE loop that fetches data into an ixRow and then inserts the ixRow into the ixRowArray (line 38). The ixSQLStmt class provides both the fetch() and fetchInto() member functions. The fetch() function instantiates a new ixRow object each time it is called, whereas fetchInto() updates the values in the nominated ixRow. If you're expecting multiple rows of data, fetchInto() is faster.

Line 28 illustrates the use of the SQLError() function (of the connection object) to retrieve error information from the database server.

That's all there is to using the ixSQLStmt class! As you can see, there is a fair bit more to this operation than to the equivalent operation in embedded SQL. That's the price you have to pay to achieve multi-database access. This process is not quite as bad as it might seem, however. Note that most of the variables in Listing 40.23 are reference variables and, therefore, can be passed by reference into and out of a function. A single function in your application can handle the execution and error checking for all SQL operations.


TIP: Develop a SHARED application function that executes all the SQL statements. Error checking (and logging) can get quite involved and could become tedious if not modularized.

Parameters for Prepared Statements

You have seen how you can set parameters using setParam(). You can also use the setParams() function. The prototype is

setParams(rowParams ixRow) RETURNING VOID

This function is slightly more efficient but, importantly, allows you to set all the parameters in a generic fashion (meaning that one function can set a multiple number of parameters).

Database Cursors

The ixSQLStmt class supports named cursors. You can set a name for an ixSQLStmt object by calling the setCursorName() member function. The prototype is

setCursorName(name CHAR(*)) RETURNING VOID

You can determine the cursor name of an ixSQLStmt using the getCursorName() member function. NewEra assigns a default cursor name. You must call setCursorName() prior to prepare() or execDirect().

Named cursors are particularly useful when you're using a FOR UPDATE statement in your SQL. They allow you to issue an update SQL statement utilizing the WHERE CURRENT OF cursor_name syntax.

The SQL Communication Area: ixSQLCA

You do not call the constructor of the ixSQLCA object directly; the constructor is called by the getSQLCA() member function of the ixSQLConnect class. An ixSQLCA object is meaningless unless it is associated with a database connection (ixSQLConnect).

The ixSQLCA has the members shown in Table 40.13.

Table 40.13. ixSQLCA members.

Members Use
SQLAWARN The SQLAWARN member variable is an eight-place character string. Normally, all characters are blank. However, after certain database operations, some of the characters can be set to "W" (for warning). If any of the characters are set to "W", the first character, SQLAWARN[1], is set to "W". So you can test for this first.
SQLAWARN[2] is set if a database with transactions is opened or a data value is truncated to fit a character.
SQLAWARN[3] is set if an ANSI-compliant database is opened or a NULL is encountered in an SQL statement.
SQLAWARN[4] is set if an INFORMIX-OnLine database is opened or the number of data values in an SQL select is not the same as the number of INTO variables.
SQLAWARN[5] is set if a float-to-decimal conversion occurs.
SQLAWARN[6] is set when an extension to the ANSI/ISO standard is executed, and the DBANSIWARN environment variable is set.
SQLAWARN[7] and SQLAWARN[8] are not used.
SQLCODE An integer that records the status code of the last SQL operation. A value of zero indicates a successful operation. A value of 100(NOTFOUND) indicates that a SELECT operation found no rows. A negative value indicates a failure.
SQLERRD An array of six integers.
SQLERRD[1] is not used.
SQLERRD[2] is set to the last ISAM error code or serial number generated by the SQL operation.
SQLERRD[3] is the number of rows processed.
SQLERRD[4] is the estimated CPU cost for the query.
SQLERRD[5] is the offset of the error into the SQL statement.
SQLERRD[6] is the ROWID of the last row.
SQLERRM Not used.
SQLERRP Not used.

The implicit connection object is automatically assigned the global record SQLCA (not an ixSQLCA object). You can create an ixSQLCA object for the implicit connection if required.

If you have previously created an ixSQLCA object for a connection, it is not automatically updated after a database operation. After a database operation, you need to create an ixSQLCA object by calling getSQLCA() for the ixSQLConnect object that you want to test.

Stored Procedures

Stored procedures are executed like any other SQL statements using either embedded SQL statements or ixSQLStmt objects. Stored procedures are important to the client/server application. Most client/server applications make extensive use of stored procedures to minimize network traffic.


TIP: Informix stored procedures support named parameters, and I recommend that all stored procedures be used with them.

Summary

This chapter covered the basic syntax of the NewEra language. You learned about NewEra as a procedural language and as an object-oriented language. You saw that NewEra supports both procedural and object-oriented styles of development.

Finally, because NewEra is a database-centric language, you learned about the facilities NewEra provides for database access.


Previous chapterNext chapterContents


Macmillan Computer Publishing USA

© Copyright, Macmillan Computer Publishing. All rights reserved.