It is rather old (I wrote it for The Aquarium prior to the 5.0 release) but somewhat applicative today. It may be hard to find but look fora copy of my book. It shows how to write Clipper applications and demonstrates a method which emphasizes writing the least amount of code to obtain the most flexible, reusable solutions.
They're mysterious they're fun they're often misunderstood they're code blocks
by Tom Leylan
Clipper 5.0 is almost here and with it comes a zillion or so new things to learn. Among them something called a "code block", but before we can discuss what code blocks do we need to agree on what code blocks are.
What Code Blocks Are
Code blocks are a new datatype. This is important, they are not procedures, they are not functions, they are not objects, they are a new datatype and they act like data. They happen to contain compiled Clipper code. Nantucket describes them as "assignable unnamed functions".
Permit me a momentary digression.
We accept the fact that characters are a datatype and we likely agree that a bunch of characters in a row (called a character string) is a datatype, perhaps as Clipper programmers we don't think of the two as particularly different.
There is no doubt that we consider numbers a datatype and in the Clipper / dBase world we are led to believe that dates and logical variables are unique datatypes also.
One might suspect that defining datatypes is something best left to Computer Science types or that there is some listing of acceptible datatypes in a old binder somewhere and that everyone agrees that this is the sum total of them. Perhaps some people think that datatypes are somehow tightly connected to the design of computers and that they need to be particularly discreet little chunks of stuff easily represented on the screen.
None of this could be further from the truth. Characters, numbers, dates and logicals were chosen because they represented things that we all find useful. Datatypes could easily be rendered for telephone numbers and zipcodes though one could argue that they are just a patterned collection of numbers... but then so are dates.
Inventing a datatype requires only the definition of acceptible values and one or more operations that can be carried out on that datatype.
We can "add" numbers and we can "add" strings but the results are considerably different. We can "divide" two numbers but we cannot divide two strings. One the other hand we can convert a string to upper case but that operation is meaningless when applied to numbers.
Code blocks are essentially data, the data is executable code, but because they act like data it doesn't mean that you can automatically add or subtract them or convert them to upper case. Code blocks have their own set of operations appropriate to code blocks.
Operations On Code Blocks
Variables can be initialized to specific datatypes and can be assigned values of that datatype.
sVar := "this is a string" // string
nVar := 5 // numeric
dVar := date() // date
tVar := .T. // logical
aVar := { 1, 2, 3, 4 } // array
It turns out that we can initialize and assign code blocks also.
bVar := {|| Qout("A Hello World Code Block")}
Notice that a code block is contained within curly braces ({}) the same characters used to contain arrays. To distinguish them from arrays, code blocks require the inclusion of a set of two vertical bars (||). These bars also serve to delimit the formal parameters but more on that later.
We can "add" numbers, "concatenate" strings, "increment" dates, "invert" logicals, "sort" arrays and we can "execute" code blocks.
Using the proper terminology we can "evaluate" code blocks and the way we do it is through the EVAL() function or through a number of other functions that accept a code block as a parameter.
Notice that each datatype has some operation that is unique. This is quite probably the reason that we find any particular datatype worthwhile. If, for instance I could add the strings "123" + "456" and obtain an answer of "579", the need for a numeric datatype would be lessened.
Code blocks just like all other datatypes can be passed as arguments to and can be returned from functions. In their ASCII representation they can saved to a file on the disk or stored in a .DBF field.
Evaluating Code Blocks
Let's EVAL() our first code block example.
cls
bVar := {|| Qout("A Hello World Code Block")}
eval(bVar)
return
Enter, compile, link and run this example and with any luck at all the phrase "A Hello World Code Block" appears on the screen. Never one to trust in luck... if the message did not appear on your screen check everything (you are using Clipper 5.0 right ?) and try again.
You can simply EVAL() the code block directly by the way.
cls
eval( {|| Qout("A Hello World Code Block")} )
return
Not to be overly confusing but someone has undoubtedly noticed that our simple example would continue to operate identically if we coded it more traditionally as follows:
cls
Qout("A Hello World Code Block")
return
The QOUT() function by the way is the equivalent of the ? command in function form (a quick look at STD.CH will confirm this for the doubting among you).
Well If I Can Just Use a Function What's The Point ?
Hang on, not so fast... stick around, I'll show you more.
Don't get the idea that code blocks are equated with a method for printing values on the screen, they are not. While possible, (we proved that), that alone wouldn't serve much purpose.
A code block can just return a number
cls
Qout( eval( {|| 5 } ) )
return
A code block can perform math and return the answer
cls
? eval( {|| 5 * 5 } )
return
A code block can call another function
cls
@ 10, 5 say eval( {|| MyUdf() } )
return
function MyUdf
return "my udf return string"
Notice that in each case I have had to output the value returned by EVAL(). The EVAL() function works in that way just like any other function. EOF() for instance, returns the answer it doesn't automatically print it on the screen.
Like EOF(), RECNO() and all other Clipper functions you can assign the value returned by EVAL() to another variable.
cls
sVar := eval( {|| MyUdf() } )
@ 10, 5 say sVar
return
function MyUdf
return "my udf return string"
We can instead return the return value of the EVAL() function
cls
@ 10, 5 say MyUdf()
return
function MyUdf
return eval( {|| "my udf return string" } )
And as was mentioned earlier, code blocks themselves can be returned
cls
@ 10, 5 say eval( MyUdf() )
return
function MyUdf
return {|| "my udf return string" }
It Still Looks Pretty Trivial
Well then let's add some parameters.
cls
? eval( {| nVar | nVar * nVar }, 6 )
return
In this case we have designed the block to accept a block parameter named nVar which is squared and returned. We have passed along the value 6 as the second argument in the EVAL() function. nVar will be assigned the value 6 and the answer 36 will be returned and displayed.
Talk of parameters and code block variables brings up an important question. What scope do the block parameters have ? And the answer is that they have LOCAL scope which the following fragment should demonstrate.
cls
nVar := 1
? eval( {| nVar | nVar := nVar * nVar }, 6 )
? nVar
return
The answers 36 (actually 36.000000000) and 1 are displayed which indicates that the assignment of 6 to nVar within the code block didn't affect the PRIVATE nVar which was assigned outside of the code block.
On the other hand, barring any naming conflicts variable references inside of the block take on the scope of the variables as defined outside of the block.
cls
nVar := 6
? eval( {|| nVar := nVar * nVar } )
? nVar
return
In this case nVar is displayed as 36 each time because the nVar within the block is the same variable as the one outside the block.
With some slightly convoluted code we can demonstrate another related and important plus about variable scope and code blocks.
local nVar
cls
nVar := 2
bSquare := {| nNum | nNum ^ nVar }
? MyUdf(bSquare)
return
function MyUdf(bBlock)
return eval( bBlock, 10)
The answer displayed is 100 which might seem just a little bit odd because nVar was declared as LOCAL to the main routine but it was still visible in the MyUdf() function. Ordinarily that is behavior demonstrated by PRIVATE and PUBLIC scoped variables only.
It is referred to as exporting a variable and it happens because both nVar and the block referring to nVar are defined in the same routine. When the code block is passed as a parameter to another function nVar (in this example) is made visible to the function.
It Gets A Bit Trickier
Multiple parameters can be passed along.
cls
sRet := eval( {| dVar, sVar | Qout(sVar, dVar) }, date(), "today is :")
? sRet
return
Compiling, linking and running this example will yield something a little unusual. The value "NIL" is output on the line, ? sRet.
NIL is the return value of the Qout() function (it says so in the documentation). NIL, by the way, is another new datatype whose only value is NIL but that is a subject for another time. We don't have to display the return value of the EVAL() function of course but I thought that you might want to see it.
Multiple expressions can be included within a code block (but you cannot include commands (they aren't expressions)).
cls
sRet := eval( {| nVar1, nVar2 | ++nVar1, --nVar2 }, 0, 0)
? sRet
return
Two numeric zero values were passed to the code block which assigned them to the formal parameters nVar1 and nVar2. The first expression incremented nVar1, the second expression decremented nVar2, (the comma is used to separate expressions). When there are multiple expressions in a code block the "value" of the code block is the value of the last expression and in this example, sRet was assigned the value of nVar2.
And don't be overly discouraged by the "no commands" restriction, because we can include functions we can easily write a UDF that executes as many commands as we like.
cls
sRet := eval( {|| MyUdf() } )
return
function MyUdf
cls
@ 10, 5 say "This is a test"
return 0
Gets Rid Of A Few Macros
Code blocks themselves can be passed as parameters which is arguably their strongest point. The closest we've been able to come to this kind of functionality in Summer '87 is through the use of macros to express a function name.do case
case iKey == "A"
sFunc := "addrec()"
case iKey == "D"
sFunc := "delrec()"
endcase
&sFunc.
The unnecessary use of macros is always best avoided and using blocks would be ideal here.
do case
case iKey == "A"
bFunc := {|| addrec() }
case iKey == "D"
bFunc := {|| delrec() }
endcase
eval( bFunc )
They're Used Within Clipper
It isn't a poor guess to suppose that we have access to code blocks because the developers of 5.0 needed them. They are used extensively in 5.0 and can be seen being referenced in the STD.CH file.
The following list of commands reference code blocks :
SET FORMAT TO
@ SAY/GET RANGE
MENU TO
SET KEY TO
COUNT
SUM
AVERAGE
DELETE FOR/WHILE
RECALL FOR/WHILE
REPLACE WITH FOR/WHILE
Additionally some Clipper functions have been enhanced to allow code blocks as arguments.
ASCAN()
ASORT()
SETKEY() // this one is new
VALTYPE() // this one is new also
While most of the commands have hidden the internal workings of the code block a few of them use documented "iterator" functions.
Iterator Functions
AEVAL() and DBEVAL() are iterative functions. Essentially what they do is EVAL() a code block against each element of an array (AEVAL()) or each record of a .DBF file (DBEVAL()).
To use the AEVAL() function you must pass at least two arguments, the first is the array name and the second is the block to execute.
aVar := { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
bDouble := {| iVal | Qout( transform( (iVal * 2), "999" )) }
aeval( aVar, bDouble )
This example displays the value of each element after it has been doubled. I tossed in the transform() function to "add some spice" and to eliminate those trailing zeros.
Optionally third and/or fourth arguments can be passed to AEVAL() that restrict the range of elements to be processed. The third argument is the index into the array representing the first element to apply the block to (if you don't pass it the default is 1).
The fourth argument is the number of elements to process, (not the last element, the number of elements). If you don't pass this along the default is all the elements in the array.
aVar := { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
bDouble := {| iVal | Qout( transform( (iVal * 2), "999" )) }
aeval( aVar, bDouble, 5, 3)
This example restricts the elements to be "blocked" to the 5th, 6th and 7th elements.
Lets have a "real" example shall we ?
/* CDIR.PRG
From an idea presented by Bill Christison on NANFORUM and rather heavily modified by Tom Leylan
Some work remains to make it operate "properly" but when I wrote this there was a bug in the directory() function which made my DirAdj() function necessary
compile with : Clipper cdir /M /N /W */
#define F_NAME 1
#define F_SIZE 2
#define F_DATE 3
#define F_TIME 4
#define F_ATT 5
FUNCTION main(sParm1, sParm2)
local sPath, tPause
do case
case pcount() == 0
sPath := "*.*"
tPause := .f.
case pcount() == 1
if (upper(sParm1) == "/P")
sPath := "*.*"
tPause := .t.
else
sPath := DirAdj(sParm1)
tPause := .f.
endif
case pcount() == 2
sPath := DirAdj(sParm1)
tPause := (upper(sParm2) == "/P")
endcase
Qout()
aeval( directory(sPath, "d"), {|aFile| DirSay(aFile, tPause)} )
return NIL
/* this is incomplete but should not be necessary */
FUNCTION DirAdj(sParm1)
local sPath := ltrim(rtrim(sParm1))
if right(sParm1, 1) == "\"
sPath := sParm1 + "*.*"
elseif right(sParm1, 1) == ":"
sPath := sParm1 + "\*.*"
elseif right(sParm1, 1) == "."
sPath := sParm1 + "\*.*"
endif
return sPath
/* list the files */
FUNCTION DirSay(aFiles, tPause)
static nlines := 0
if nlines >= 23 .and. tPause
QOut( "Strike a key when ready . . . " )
inkey(0)
nlines := 0
endif
nlines++
QOut( DirFile(aFiles[F_NAME]), " ", ;
DirSize(aFiles[F_SIZE], aFiles[F_ATT]), ;
DirDate(aFiles[F_DATE]), " ", ;
DirTime(aFiles[F_TIME]) )
return NIL
/* dump the dot */
FUNCTION DirFile(sFile)
local sTemp, iDot, sName, sExt
if left(sFile, 1) == "."
sName := left(sFile + space(12), 12)
else
sTemp := sFile + " ."
iDot := at(".", sTemp)
sExt := left(subs(sTemp, iDot + 1) + space(3), 3)
sName := left(subs(sTemp, 1, iDot - 1) + space(9), 9) + sExt
endif
return sName
/* could be a directory */
FUNCTION DirSize(iSize, sAtt)
return if( ("D" $ sAtt), "
", str(iSize, 8))
/* date delimiters are screwy */
FUNCTION DirDate(dDate)
local sDate := strtran(dtoc(dDate), "/", "-")
return str(val(left(sDate, 2)), 2) + right(sDate, 6)
/* convert to 12 hour format */
FUNCTION DirTime(sTime)
local iHr, sRet
iHr := val(left(sTime, 2))
if (iHr <>
sRet := str(iHr + if((iHr == 0), 12, 0), 2) + subs(sTime, 3, 3) + "a"
else
sRet := str(iHr - if((iHr == 12), 0, 12), 2) + subs(sTime, 3, 3) + "p"
endif
return sRet
Compile and link this example and you have a DOS directory program.
To use the DBEVAL() function you must have a .DBF file open in the currently selected work area and you must pass at least one argument which is the block to execute against each record.
use test
/* test.dbf must have the following structure and you should have some data in it
NAME C 30 0
CITY C 20 0
STATE C 2 0
*/
bShow := {|| Qout( NAME, CITY, STATE ) }
dbeval( bShow )
close test
Optionally there are five other arguments corresponding to
a FOR condition
a WHILE condition
a NEXT n records specifier
a RECORD n specifying a single record number
a REST logical value used to indicate whether the record pointer should be rewound prior to beginning the process or whether to process only the remaining records
I will have to avoid giving any explicit details about the iterator functions like AEVAL() and DBEVAL() in this article. They honestly deserve a separate article with more substantial examples.
And That Isn't All
Except for the example which demonstrated eliminating the macroing of functions it is likely that you cannot think of one overwhelming reason to use a code block instead of a function call. Don't let this bother you. Code blocks are not intended to replace functions they are not the "greatest thing on Earth", they are what they are they do what they do and their best use is where they are needed and not where they can be molded to fit.
We will certainly see their use enhanced in future versions of Clipper I think the ACHOICE(), DBEDIT() and MEMOEDIT() functions are likely candidates but well before then I'm certain that we will see numerous good (and probably some not so good) examples published.
Please don't rush headlong into using code blocks but also don't back away... just take them in stride like all the other things that make Clipper "Clipper".
About the author: Tom Leylan has been a guest speaker at conferences around the world including events in Amsterdam, London, Sydney, Johannesburg, Miami Beach, Orlando, Los Angeles, Palm Desert and Honolulu. He has written numerous articles and is the author of the book, Writing Applications With Clipper (c) 1994 MIS Press ISBN: 1-55851-382-5.
Tom can be reached electronically at: tleylan@nyiq.net
No comments:
Post a Comment