Thursday, April 22, 2021

Writing Better Code (3)

Writing Better Code (3)

In the third article of this little series I am going to talk about Procedures and Functions. Visual FoxPro, like its ancestors FoxPro and FoxBase supports two different ways of declaring, and calling, code.

Creating a Procedure

A procedure is simply a block of code that is addressed by name. It may, but is not required to, accept one or more parameters and the reason for creating a procedure is to avoid the necessity of writing the same code in many places. Procedures are called using the “DO” command and, by default, any parameters are passed by reference. There is, therefore no need to have procedures return  values – they can modify any values in the calling code that are passed to them.
Here is a simple example of the sort of code you might put into a procedure. All it does is to accept a Record Number and Table Alias. It validates that the record number is valid in the context of the specified table and, if so, moves the record pointer. (Note: the code, as written, does not take account of buffered records - that have a negative recorde number).  If anything fails, or is invalid, an error is generated:

********************************************************************
*** Name.....: GOSAFE
*** Author...: Andy Kramek & Marcia Akins
*** Date.....: 03/20/2006
*** Notice...: Copyright (c) 2006 Tightline Computers, Inc
*** Compiler.: Visual FoxPro 09.00.0000.3504 for Windows
*** Function.: If the specified record number is valid in the
*** .........: specified alias go to it, otherwise generate an error
********************************************************************
PROCEDURE gosafe
PARAMETERS tnRecNum, tcAlias
TRY
  *********************************
  *** Validate the parameters first
  *********************************
  *** We must get a record number!
  IF VARTYPE( tnRecNum ) # "N" OR EMPTY( tnRecNum )
    ERROR "Must pass a record number to GoSafe"
  ENDIF

  *** If no Alias specified, assume current alias
  lcAlias = IIF( VARTYPE( tcAlias ) # "C" OR EMPTY( tcAlias ), LOWER( ALIAS()), LOWER( ALLTRIM( tcAlias )) )
  IF EMPTY( lcAlias ) OR NOT USED( lcAlias )
    *** No table!
    ERROR "Must specify, or select, an open table when calling GoSafe"
  ENDIF

  *********************************
  *** Check that the record number is valid for the alias
  *********************************
  IF BETWEEN( tnRecNum, 1, RECCOUNT( lcAlias ) )
    *** This is OK
    GOTO (tnRecNum) IN (lcAlias)
  ELSE
    *** Nope, the record number is no good
    ERROR "record " + TRANSFORM( tnRecNum ) + " is not valid for table " + lcAlias
  ENDIF    

CATCH TO loErr
  MESSAGEBOX( loErr.Message, 16, "GoSafe Failed" )

ENDTRY
RETURN
To set up and call the procedure we use:

SET PROCEDURE TO procfile ADDITIVE
*** Save the record pointer in Account
lcOldAlias = ‘account’
lnOldRec = RECNO( lcOldAlias )
<<more code here>>
*** Restore the record pointer
DO GoSafe WITH lnOldRec, lcOldAlias
This particular procedure doesn’t modify anything and although in fact (like all VFP Methods, Functions and Procedures) it actually returns a logical “.T.” there is no need, or even facility, to capture that value. Note that if we wanted to pass values to a procedure by value, instead of by reference, then we have to pass them as the actual values and not as variables (Yes, we could also change the setting of UDFPARMS but that is NOT recommended because it has other, usually unwanted, side-effects. Besides, using a global solution for a local issue is generally a bad idea anyway). So to pass a record number ‘by value’ to this procedure we would use:
DO GoSafe WITH INT( lnOldRec ), lcOldAlias

Creating a Function

The other method for calling code is to create a function. The basic difference between a procedure and a function is that functions ALWAYS return a value and so whenever you call a function, whether it is a native VFP function, or one of your own, you should always check the result. Functions are called by suffixing the name with a pair of parentheses. As with procedures, functions may accept one or more parameters but unlike procedures, parameters passed to functions are, by default, passed by value. The consequence is that functions do not modify values in the code that called them. (Yes, this is the exact opposite of the behavior for procedures). Here is a simple function that returns the time as a character string from a number of seconds.

********************************************************************
*** Name.....: GETTIMEINWORDS
*** Author...: Andy Kramek & Marcia Akins
*** Date.....: 03/20/2006
*** Notice...: Copyright (c) 2006 Tightline Computers, Inc
*** Compiler.: Visual FoxPro 09.00.0000.3504 for Windows
*** Function.: Return number of Days/Hours and Minutes from a number of seconds
*** Returns..: Character String
********************************************************************
FUNCTION GetTimeInWords( tnElapsedSeconds, tlIncludeSeconds )
LOCAL lcRetval, lnDays, lnHrs, lnMins
*** Initialize the variables
STORE '' TO lcRetval
STORE 0 TO lnDays, lnHrs, lnMins*** Handle the Days first
lnDays = INT( tnElapsedSeconds / 86400 )
IF  lnDays > 0
  lcRetVal = PADL( lnDays, 3 ) + ' Days '
ENDIF
*** Next the hours
lnHrs = INT(( tnElapsedSeconds % 86400 ) / 3600 )
IF lnHrs > 0
  lcRetVal = lcRetVal + PADL( lnHrs, 2, '0' ) + ' Hrs '
ENDIF
*** Now the minutes
lnMins = INT(( tnElapsedSeconds % 3600 ) / 60 )
*** And check for seconds
IF tlIncludeSeconds
  *** If we want the seconds - add them exactly
  lcRetVal = lcRetVal + PADL( lnMins, 2, '0') + ' Min '
  lcRetVal = lcRetVal + PADL( INT( tnElapsedSeconds % 60 ), 2, '0' )+' Sec '
ELSE
  *** Round minutes UP if >= 30 seconds
  lnMins = lnMins + IIF( INT( tnElapsedSeconds % 60 ) >= 30, 1, 0 )
  lcRetVal = lcRetVal + PADL( lnMins, 2, '0') + ' Min '
ENDIF
RETURN lcRetVal
As you can see, unlike the procedure, this code actually creates, and explicitly returns, a character string to whatever calls it. (The rationale behind this function is, of course, that if you obtain the difference between two DateTime values the result is in seconds). So when calling this function the return value must be handled in some way. We can display it:
?  GetTimeInWords( 587455, .T. )  && Displays:  6 Days 19 Hrs 10 Min 55 Sec
or store it to a variable:
lcTime = GetTimeInWords( 587455 )
orevn the clipboard (to paste into some other application for example)
_cliptext = GetTimeInWords( 587455 )
Remember that the default behavior of Visual FoxPro is that parameters are passed by value to a function so if you do need to pass values by reference (and this most commonly arises when dealing with arrays) then you must pass prefixing the reference with an “@” sign, like this:
DIMENSION gaMyArray[3,2]
luRetVal = SomeFunction( @gaMyArray )

So what’s the difference

Although I declared this code as a FUNCTION and the GoSafe code as a PROCEDURE, in practice VFP does not care. We can call code that is declared as a procedure as if it were a function (and vice versa). Thus we can actually call the “GoSafe” procedure referred to above like this:
llStatus = GoSafe( lnOldRec, lcOldAlias )
The return value in llStatus would, given the way the procedure is written ALWAYS be .T. but if we modified the code just slightly, we could have the return value reflect whether or not the procedure succeeded in setting the record pointer correctly. The only modification needed is to test for success after moving the record pointer, thus:

  IF BETWEEN( tnRecNum, 1, RECCOUNT( lcAlias ) )
    *** This is OK
    GOTO (tnRecNum) IN (lcAlias)
    *** Check that we ended up on the right record
    *** If so, set Return Value = .T.
    STORE ( RECNO( lcAlias ) = tnRecNum ) TO llRetVal
  ELSE
    *** Nope, the record number is no good
    ERROR "record " + TRANSFORM( tnRecNum ) + " is not valid for table " + lcAlias
  ENDIF    

CATCH TO loErr
  MESSAGEBOX( loErr.Message, 16, "GoSafe Failed" )
  *** Force the return Value to be false in all errors
  llRetVal = .F.
ENDTRY
*** Return Status
RETURN llRetVal
Of course we could also run the GetTimeInWords function by calling it as a procedure:
DO GetTimeInWords WITH 587455, .T.
But it wouldn’t do us much good, because the function does nothing apart from return a value – which in this case we cannot detect. The interesting thing about all this is that it makes two points:
·         First, VFP doesn’t actually care whether something is written as a PROCEDURE or as a FUNCTION. It is just code to be executed. What does matter is whether the code is called as a procedure (using DO gosafe ) or as a function ( llStatus = gosafe() )
·         Second, if you are calling a function it is vitally important to check the return value. While procedures usually embody their own error handling (as in the example given here), the value returned by a function is usually the only indication that whether it did what was expected, or not. This is true whether the function in question is defined in a procedure file, as a stand-alone program, a method on an object or a native Visual FoxPro function.

What has all this to do with writing better code?

Now you may be thinking that this is all very basic, and obvious, so why am I bothering with it. But you know, one of the commonest things that I see on the various forums that I frequent is a message titled something like:
TableUpdate Not Working
Apart from the obvious comment that it is highly unlikely that an error in the operation of something so fundamental as TableUpdate() would get through testing and into a released version of VFP, what this really means is that “I cannot get TableUpdate to work”.  Usually the question develops along these lines:
When users change the data, their changes are visible in the form but are not being saved to the database. There is no error, but the table is not updated. Please help!
And when the person finally posts their code (often after much prompting – why are some people so reluctant to post their code?) we find this:
=TableUpdate( .T. )
Now this one single line of code, that has been in every version of the VFP Help file (yes, including VFP 9.0) is, I believe, responsible for more wasted developer hours than almost anything else ever written. What’s wrong with it? Well, let’s see:
First, TableUpdate() is a function, which as we have seen, means that it returns a value. Where is the check for that value? Even though the help file clearly states that it has a return value there is no indication in the examples in the help file that you need to check it! However, it is clear, when you read the fine print, why it is IMPERATIVE that you check the return value:
Return Value
Logical data type. TABLEUPDATE( ) returns True (.T.) if changes to all records are committed. Otherwise, TABLEUPDATE( ) returns False (.F.) indicating a failure. An ON ERROR routine isn't executed. The AERROR( ) function can be used to retrieve information about the cause of the failure.

What does this mean? Quite simply that if a call to TableUpdate() fails to update a record the ONLY way you will know is if you test the return value.
Second, it is passing only one of the FOUR possible parameters for TableUpdate(), not the least important of which is the third, (the name of the table that is supposed to being updated).
Third, it uses the old (VFP 3.0, VFP 5.0) logical value for the first parameter, despite the fact that the possibility of using extended functionality was introduced as far back as Version 6.0! In fact about the only thing right about this example code is the spelling of the word “TableUpdate”!
How should this example look? Like this!

llOk = TABLEUPDATE( 1, .F., 'employee' )  && Commits changes to current row only
IF llOk
  *** TableUpdate Succeeded
  ? 'Updated cLastName value: '
  ?? cLastName  && Displays current cLastName value (Jones).
ELSE
  *** TableUpdate Failed - WHY?
  AERROR( laErr )
  MESSAGEBOX( laErr[ 1, 2], 16, 'TableUpdate Failed' )
ENDIF

For full details on the use of TableUpdate() (and TableRevert()) see my blog article “Handling buffered data in Visual FoxPro” from December 2005.

Conclusion

The point here is that you MUST get into the habit, if you do not already have it, of checking the return values from function calls – even those whose result you KNOW in advance (how many times do you use the SEEK() function but NOT check the result? Be honest!).
The only possible exception I can think of, off-hand, would be TableRevert() because I am not sure how it can possibly fail, or even what you can do about it if it does. This seems to be more of a command, or  procedure, than a true function although, of course, it does return a value (i.e. the number of rows reverted).


Published Monday, March 20, 2006 7:47 PM by andykr

No comments:

Post a Comment

Writing better code (Part 1)

Writing better code (Part 1) As we all know, Visual FoxPro provides an extremely rich and varied development environment but sometimes to...