Returning a Dimensioned Variable from a Procedure
by Unknown user
A recent news group posting asked how to return a dimensioned variable, i.e. an array, from a called procedure. There is no “short answer” to this question … okay, there is and it is “you can't.” It is possible to get an array of values back from a called procedure. It just can't be done as a return value.
That is, a procedure prototyped as:
CalledProc( ... ),Long
where the Long is an array will fail to compile. Whether the array demarcators (“[“ and “]”) are included or not included, regardless of where they are included or omitted, the procedure will not compile.
But Clarion provides another way of getting data from a called procedure, a callee, back to its caller. Of course, global variables are a possibility, theoretically, but they are neither necessary nor advisable.
As discussed in “Returning Multiple Values,” passing a variable by address (i.e., as a “variable parameter”) allows a called procedure to change the values in the callee's “copy” of the variable. (It's nor really a copy, both procedures work with the same memory location, that is, on the same variables.)
Arrays can be passed from one procedure to another. So it seems logical that passing an array by address would fulfill the requirement of getting back a dimensioned variable from a procedure.
The Language Reference explicitly recommends this for passing and retrieving arrays. This is what it has to say on the subject:
To pass an entire array as a parameter, the prototype must declare the array's data type as a Variable-parameter ("passed by address") with an empty subscript list. If the array has more than one dimension, commas (as position holders) must indicate the number of dimensions in the array. The calling statement must pass the entire array to the PROCEDURE, not just one element.
To pass an entire array as a parameter, the prototype must declare the array's data type as a Variable-parameter ("passed by address") with an empty subscript list. If the array has more than one dimension, commas (as position holders) must indicate the number of dimensions in the array. The calling statement must pass the entire array to the PROCEDURE, not just one element.
It gives the following sample prototype:
AddCount PROCEDURE(*LONG[,] Total,*LONG[,] Current) !Passing two 2-dimensional arrays
and this example code for the procedure:
AddCount PROCEDURE(*LONG[,] Total,*LONG[,] Current) !Procedure expects two arrays CODE LOOP I# = 1 TO MAXIMUM(Total,1) !Loop through first subscript LOOP J# = 1 TO MAXIMUM(Total,2) !Loop through second subscript Total[I#,J#] += Current[I#,J#] !increment TotalCount from CurrentCnt END END CLEAR(Current) !Clear CurrentCnt array
This sample code highlights not only the “how to” but ways in which arrays are different from other variables.
Prototyping
Procedures receiving arrays are prototyped a little bit differently than typical procedure calls. The entire array must be passed, per the Language Reference, not just a single element. But instead of passing anything in the array delimiters, empty brackets are used. So, for a single dimensional array:
MyProc[]
For two dimensions:
MyProc[ , ]
three dimensions:
MyProc[ , , ]
and so on. As the documents state, the subscript list must contain enough commas to define the size of the array but nothing more than that.
In the AppGen, this looks like this:
Figure 1: Prototype array-receiving procedure in the AppGen
Calling
In hand code, from the example code, AddCount is called as follows:
TotalCount LONG,DIM(10,10) CurrentCnt LONG,DIM(10,10) CODE AddCount(TotalCount,CurrentCnt)
The AppGen fully supports the convention for passing arrays:
Figure 2: Button Actions from Window Formatter:
Notice that SV's sample code does not include the bracket delimiters in the call but that I did in my demo app (downloadable at the end of this article – it is built in 8.0.8778, all runtimes are included so that everyone may run it). The application generator is happy with or without the brackets.
Receiving and Using the Array
Where dimensioned variables differ from other received parameters is in how I must handle the variable(s) in the called procedure.
Consider the prototype of the procedure shown in Figure 1, above:
NextProc PROCEDURE (*Decimal[] pDatum)
“pDatum” is the Label of my “local” variable. If I just want to make a calculation involving pDatum, I can us pDatum just like any other dimensioned variable. This is shown in the help code snippet shown above.
But, if it is necessary to assign the parameter to a (real) local variable, in order, for example, to use the window Formatter to display the incoming values on a window, things change. Arrays are different in that simply assigning the parameter to a local variable:
LOC:Datum[] = pDatum[]
is illegal. So is:
LOC:Datum = pDatum
(in other words, the compiler will … uh, complain).
And while we're on the subject, what if the passed array and the local, receiving, array have different numbers of parameters? Seriously bad things could happen trying to assign a value to an index that doesn't exit.
Clarion provides a function that allows checking the highest index in a DIM declaration, MAXIMUM. So,
If Maximum(pDatum,1) > Maximum(LOC:Datum,1)
will verify that there are not enough local elements to receive all the elements of the parameter (it seems to me that it would be acceptable for the local variable to support more indecis; it is only fewer that will cause trouble).
If Maximum(pDatum,1) > Maximum(LOC:Datum,1) Message('The local variable has ' & Maximum(LOC:Datum,1) & | ' elements. But the incoming parameter has ' & | 'Maximum(pDatum,1) & '.||This will ' &| 'cause an Index Out of Range error.','Array ' & | 'Mismatch',ICON:Hand)
in ThisWindow.INIT, therefore, can be used to terminate the call should my local variable not support sufficient subscripts. (I probably don't need these code after initial testing, data declarations don't usually change specifications at run time; but neither does leaving the check hurt anything.)
At the same time, because I am assigning a local version of the array, I can ensure that I don't accidentally make an invalid assignment by determining which of the two arrays is smaller:
If Maximum(LOC:Datum,1) >= Maximum(pDatum,1) C = Maximum(LOC:Datum,1) Else C = Maximum(pDatum,1) End
and use my “size” variable to control the assignment:
Loop NDX = 1 to C LOC:Datum[NDX] = pDatum[NDX] End
by not making any assignment where one of the variables could go out of range. This is important. Tracking down out of range array indecis can be quite an adventure. Often, an out of range index does not throw an error, the app simply GPFs.
Similar code can be used to safely reload the passed variables to update the values visible to the caller:
Loop NDX = 1 to Maximum(pDatum,1) pDatum[NDX] = LOC:Datum[NDX] End
Post(EVENT:CloseWindow) and the called procedure closes and the calling procedure will have the correct values.
Conclusion
A good news group question, a quick look at the Language Reference … and “it can't be done” once again becomes “here's how to achieve the result.” Reframing the question – from getting a “return” value to “getting the value back” – as they taught me in Philosopher school also helps.