Operations using code blocks Foundation
A code block is a special data type allowing the assignment of executable code to a variable and execution of the code at any time the program is running. Code blocks can be used in many ways and are essential for event driven programming and for the use of Xbase Parts. Operators which can be applied to code blocks are listed in the following table:
Operator | Description |
---|---|
{|| } | Delimiter for literal code blocks |
== | Test whether two code blocks are identical |
= | Comparison: equal |
!= <> # | Comparison: not equal |
= | Assignment |
:= | Inline assignment |
The characters {|| } represent the delimiting characters used to define a literal code block in program code. The program code contained in a code block is executed using the function Eval().
LOCAL cString, bBlock
cString:= "James"
bBlock := {|| cString += " Bond" }
? cString // result: James
Eval(bBlock)
? cString // result: James Bond
The code block in this example contains a concatenation operation, which attaches additional characters to a character variable. The operation is not executed when the code block is defined, but only later when the variable bBlock (containing the code block) is passed to the function Eval(). Thus, a code block allows the definition of program code to be separated from the point where the code is executed.
Code block parameters and return value
Parameters can be defined between the two characters | | in the code block definition. These parameters can accept arguments passed to the function Eval(). That is analogous to the definition of formal parameters for user-defined functions or procedures. Code block parameters are LOCAL variables which exist when the code block is executed and are only visible within the code block.
LOCAL bBlock := {|x, y| x > y }
? Eval( bBlock, 5, 10 ) // result: .F.
? Eval( bBlock, "Z", "A" ) // result: .T.
The code block in this example accepts two parameters and compares their values with one another. When the code block is defined, it is not known what values will be compared, but they are later passed to the code block "from outside" by the function Eval(). The return value of the function Eval() is the result of the comparison. This means that the expression within the code block determines its return value as well as the return value of the function Eval(). A code block can be viewed as a kind of "user-defined function": it can have parameters, it executes program code, and has a return value.
Complex code blocks
A code block can contain any number of expressions separated by commas. The expressions are processed from left to right and the return value is the value of the last expression in the code block. The expressions can be formulated in any manner and can also contain function calls. Statements, commands and declarations are not allowed. This means that no loops can be processed and no DO CASE branches can be performed within a code block. In order to process loops, a code block must call a user-defined function or procedure where the loop is programmed.
LOCAL bBlock, i
bBlock := {|nRow| SetPos(nRow,0), QQOut("Hi folks!") }
FOR i:=1 TO 10
Eval( bBlock, i )
NEXT
In this example, the code block bBlock is evaluated ten times in the FOR...NEXT loop. Within the code block, the screen cursor is positioned using SetPos() to the first column of a new screen row. The function QQOut() then displays the character string "Hi folks!" at this position. The screen row where the output appears corresponds to the value of the loop counter i passed to the code block.
Comparison of code blocks
A code block, like an array, is referenced in a variable and is not contained in the variable. Correspondingly, code block comparisons are limited to the same comparisons that can be performed with arrays. The exact equals operator == determines whether two variables reference the same code block and comparisons using the "equal" and "not equal" operators require that the other operand contains the value NIL:
LOCAL bBlock1, bBlock2, bBlock3
bBlock1 := {|nValue| nValue+10 }
bBlock2 := {|nValue| nValue+10 }
bBlock3 := bBlock1
? bBlock1 == bBlock2 // result: .F.
? bBlock1 == bBlock3 // result: .T.
? bBlock1 = NIL // result: .F.
? bBlock1 <> NIL // result: .T.
The two variables bBlock1 and bBlock2 in the example reference two different code blocks containing exactly the same program code. A comparison using the exact equals operator returns the result .F. (false) because the variables contain two different code block references. However, a comparison of the variables bBlock1 and bBlock3 returns the value .T. (true) because both variables reference the same code block. Whether two code blocks contain the same program code cannot be tested.
Create code blocks at runtime
The fact that the macro operator returns the result of a compiled character string allows code blocks to be generated at runtime of a program. A code block can be created at runtime of a program using the macro operator when the character string contains the syntax for a code block. In this case, the result of the macro operator is a code block which can be executed using the function Eval().
LOCAL aArray, cBlock, bBlock
aArray := { "James" , "Bond" }
cBlock := "{|a,i| QOut(a[i]) }"
bBlock := &( cBlock ) // compile code block
? Valtype( cBlock ) // result: C
? Valtype( bBlock ) // result: B
Eval( bBlock, aArray, 1 ) // result: James
Eval( bBlock, aArray, 2 ) // result: Bond
A code block offers enormous speed advantages over the macro operator. The code block is compiled only once and the executable code in the compiled character string can be assigned to variable. When a variable contains a code block, the corresponding code can be executed immediately by the function Eval(). If the variable contains a character string instead, the code in the character string must be compiled using the macro operator each time it is executed.
Code blocks as function return values
Since a code block can be contained in a variable, user-defined functions can also return code blocks. This has far reaching consequences in relation to lexical variables, especially LOCAL variables. Normally the lifetime of a LOCAL variable is limited to the runtime of the function in which the variable is declared. But it is possible to reference LOCAL variables in a code block and define this code block as the return value of a function. Using this technique, a LOCAL variable can remain (within a code block) after the function in which the variable is declared has terminated.
**************
PROCEDURE Main
LOCAL bBlock
SetPos(0,5) // set cursor to 0,5
bBlock := SaveCursor() // call UDF
SetPos(10,15)
? Col(), Row() // result: 15 10
Eval(bBlock)
? Col(), Row() // result: 5 0
RETURN
*********************
FUNCTION SaveCursor() // save cursor position
LOCAL nRow := Row() // screen row
LOCAL nCol := Col() // screen column
RETURN {|| SetPos(nRow, nCol) }
The user-defined function SaveCursor() saves the position of the screen cursor in two LOCAL variables: nRow and nCol. These two variables (as arguments of the function SetPos()) are tied to the code block returned by the function and assigned to a variable within the procedure Main. The variable bBlock contains a code block which still references the LOCAL variables declared in SaveCursor(). The LOCAL variables nRow and nCol are taken out of the program context of "their" function and remain within the context of the code block. A variable handled in this way is called a "Detached LOCAL".
When a function's return value is a code block, all lexical variables within the code block that are visible at the time the code block is created remain visible. The same applies to procedures and user-defined functions declared as STATIC. If they are visible at the time the code block is created, they remain visible within the code block. They can be called using the code block and the function Eval() from other functions and procedures even if they are not directly visible from the place in which the code block is used. This does not apply to dynamic variables. Access to dynamic variables can occur within a code block only when they are also visible outside the code block.
Because it provides the ability to connect any program code to a code block and execute it at any time, the code block data type allows extremely flexible functions to be created. Xbase++ provides four functions whose flexibility is due to the "code block" data type. These include three functions for processing arrays and one function for processing database files.
Function | Description |
---|---|
AEval() | Executes code block for some or all elements of an array |
AScan() | Searches in an array |
ASort() | Sorts array |
DbEval() | Executes code block for some or all data records of a file |
Eval() | Executes code block |
Type() | Returns data type via macro operator |
Valtype() | Returns data type |
Process arrays iteratively
The function AEval() performs a FOR...NEXT loop which processes some or all elements of an array. It evaluates a code block during each pass of the loop and passes to this code block the contents of the current array element and a numeric pointer to this element.
LOCAL aArray := {"a","b","c"}
AEval(aArray, {|cChar| QQOut(cChar)} ) // result: abc
In this example, the function AEval() passes each array element of aArray to a code block. The code block receives the value of an array element in its parameter cChar and outputs this on the screen using the function QQOut().
AEval() passes two parameters to the code block: the value of the current element and the numeric pointer to the element. The second parameter can be elegantly used in many cases.
LOCAL aArray[3]
AEval(aArray, {|x,i| aArray[i]:=i} ) // aArray is: {1,2,3}
The second code block parameter is used in this example to fill an array with sequential numeric values. Within the code block, the same array is accessed that was passed to the function AEval().
Search in an array
The function AScan() was shown in connection with one dimensional arrays in the previous section. When a multi-dimensional array is searched for a value or when a specific search expression is desired, a code block containing the search expression must be passed to the function AScan(). The contents of the current array element are passed to the code block by AScan():
LOCAL aArray:={"Spring","Summer","Fall","Winter"}
? AScan( aArray, "SUMMER" ) // result: 0
? AScan( aArray, {|c| Upper(c)=="SUMMER"} )
// result: 2
The first call to AScan() does not find the value "SUMMER" in the array. In the second call, the search expression specified within the code block converts each array value to uppercase letters using Upper() and then compares the result with the search value. In this case, a matching array element is found.
To search for values in multi-dimensional arrays, the code block receives a subarray and must contain the search expression within the code block.
LOCAL aArray :={ {"B",1}, ;
{"C",2}, ;
{"A",3} }
? AScan( aArray, {|a| a[1]=="A"} ) // result: 3
? AScan( aArray, {|a| a[2]==1 } ) // result: 1
In this example, a two dimensional array is searched for a value. The first call searches the first column of the array (a[1]) and the second searches the second column (a[2]). Within the code block the elements of the subarray passed to the code block by AScan() are accessed using the reference contained in the code block parameter a.
Sort arrays
By default, the function ASort() sorts one dimensional arrays in ascending order (A->Z). If arrays are to be sorted in descending order (Z->A), or if multi-dimensional arrays are sorted, the sort expression is specified using a code block. The code block receives two parameters from ASort(): the value of the current array element and the value of the next array element. The code block must return the result of a comparison operation between these values (a logical value) to ASort() for the sort to be successful.
LOCAL aArray :={ {"B",1}, ;
{"C",2}, ;
{"A",3} }
ASort( aArray,,, {|x,y| x[1] < y[1]} )
// aArray is { {"A",3}, ;
// {"B",1}, ;
// {"C",2} }
ASort( aArray,,, {|x,y| x[1] > y[1]} )
// aArray is { {"C",2}, ;
// {"B",1}, ;
// {"A",3} }
ASort( aArray,,, {|x,y| x[2] < y[2]} )
// aArray is { {"B",1}, ;
// {"C",2}, ;
// {"A",3} }
The sort expression is specified and passed to ASort() within a code block that performs a comparison between the values of two adjoining array elements. The code block receives the values of the array elements from the function ASort().
Database operations using code blocks
Records from database files can be processed iteratively using the function DbEval() in a manner similar to the function AEval() for processing arrays. DbEval() evaluates a code block for some or all records of a database. After each evaluation, the record pointer is moved to the next record until the end of file is reached or a condition signaling the end of the operation is met.
LOCAL nCount := 0
USE Customer
DbEval( {|| nCount++ }, {|| Customer->CITY = "Denver"} )
This example counts the number of customers in a customer database whose residence is in the city of Denver. The function DbEval() does not pass parameters to the code block, but it can process several code blocks at once and is therefore one of the most versatile file functions in Xbase++. Many commands are based on this function and are translated by the preprocessor into calls to DBEval() (see the file STD.CH).
Just like arrays, code blocks can be saved in files using the command SAVE and reloaded into the program using RESTORE. In order to be saved, code blocks must be referenced by dynamic memory variables (PRIVATE or PUBLIC variables). Files for memory variables have the extension XPP and replace the familiar MEM files of dBase or CA Clipper. The MEM file format is not supported by Xbase++.
If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.