In QHJ#24 and #25 there were articles on parameter passing techniques (By Tim Swenson and by Peter Tillier, respectively). I won't (too much) re-hash what's already been said (this article was prepared before I was aware of them), but look at the matter in a practical way, that may suit some readers.
I'm using SBASIC for this article - the enhanced SuperBASIC interpreter that comes with SMSQ and SMSQ/E. SBASIC behaves somewhat differently to SuperBASIC with respect to variable handling, and has some desirable features, not available in standard SuperBASIC. Where this affects the subject at hand I shall try and point out the differences. However, I am presently not able to test my examples in SuperBASIC, so some incompatibilities (ie, bugs) may be found. Please always ensure that the techniques described work with your version of S*BASIC before relying on them in any way.
There are two ways of passing parameters to functions and procedures in S*BASIC: by value, which is perhaps the "intuitive" method; and by reference, which will be the main focus of this article.
Passing parameters by value is (what we may THINK) we normally do. RUNning the program fragment below
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 test1 1,2,3 99 : 100 defproc test1(a,b,c) 110 print a,b,c 120 enddef 130 : <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
would print "1 2 3" on your screen (with any luck!). And of course:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x=1:y=2:z=3: rem Assign values to some variables 20 test1 x,y,z: rem and use these instead. <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
does the same.
But a small modification of test, test2, shows what's really going on:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 99 : 100 defproc test2(a,b,c) 110 print a,b,c 120 a=a+a:b=b+b:c=c+c:rem Double all parameter variables! 130 enddef 140 : <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
The new harness:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x=1:y=2:z=3 20 test2 x,y,z 30 print x,y,z <- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 1 2 3 <- prints out x, y, & z, as expected 2 4 6 <- but what's going on here? We set x,y,z to 1,2,3!
By altering the values of the parameter variables a, b, & c, we cause a change to the calling variables x, y, & z too. This is a call by reference; we don't pass to the procedure merely the values the variables contain, instead we refer to the original variables - a, b & c ARE x, y, z, only by a different name. As you will appreciate passing parameters by reference is not always desirable. In fact, unless you specifically want to do so, it could be a real pain: You can see how a procedure might easily (and unintended by you) alter its parameters, and thereby variables external to itself. To avoid this you can apply the rule never to alter a procedure's formal parameters within the procedure; or you must pass your variables by value only. But how to do that?
If you typed: test2 1,2,3, what do you think happens to a, b, c? Well, their values are simply thrown away when the routine returns. By extension, the same holds good for: test2 p+1,q+1,r+1, having previously set p, q, r to some value. Anything other than a variable is considered an expression in this context, and can therefore not contain a return value, ie, it cannot be altered.
Thus:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x=1:y=2:z=3 20 test2 (x),y+0,z^1 30 print x,y,z <- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 1 2 3 <- prints out x, y, & z, as expected 1 2 3 <- prints out x, y, & z, as expected(?)
Good programming practice would avoid altering the parameter variables - copy their values into LOCal variables instead! In my opinion, test2 (x),(y),(z) gives the clearest indication of intent, besides being (marginally) faster than say, x+0,y+0,z+0, and so is a good convention to adopt for call by value.
There are other "oddities" about the way parameter passing works. For example:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x$='a':y=2:z%=3 20 test3 x$,y,z% 30 print x$,y,z% 99 : 100 defproc test3(a,b,c) 110 print a,b,c 120 a = a & a : b = b / 5 : c = c / 2 130 enddef 140: <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
RUNning this program produces:
a 2 3 <- x$ y z% aa .4 1 <-
(Actually SMSQ/E 2.88 returns the wrong value for the integer divide! (2, not 1) This must be a "feature" and will no doubt be fixed in the future.)
Not what you'd think, looking at the formal parameters a, b, c! However, this can be very useful, as will be shown later. Things to watch out for though are: You may assume that the formal parameter decides the type, when it actually is the calling parameter that does so! An example might be:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x=1:y=4:z=10 20 fast_test x,y,z 99 : 100 defproc fast_test(a%,b%,c%) 110 rep loop 120 a%=a%+1:b%=b%+b%:c%=c% div 3 130 if c%=0:exit loop 140 endrep loop 150 enddef 160 : <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
You're expending all this effort optimising fast_test, changing out floating point variables with integers, and the like. You need not have bothered! This is what it's actually doing:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 120 a=a+1:b=b+b:c=c div 3 <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
In fact everything runs in (relatively) slow floating point!
The correct move is:
<- - - - - - - - - - - - - - - - - - - - - - - - - - - - -> 10 x%=1:y%=4:z%=10 20 fast_test x%,y%,z% <- - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
What you then use in the formal parameter list is irrelevent (except as a reminder as to what the correct type should be!)
In tk2 there are commands to test the parameters that are passed to a procedure: PARTYP tells you the actual parameter type (nul (never nul in SBASIC), string, float, or integer) and PARUSE whether the parameter is an array or not. (It can also be used to allow optional parameters in SuperBASIC, as PARUSE will tell you whether the variable is set or not. In SBASIC all variables are pre-initialised to nul).
A "by-product" of the ability to pass parameters by reference, is that we can actually return more than one value to the calling program. Both functions and procedures can be used for this. I find the
return_error = Function(update-able parameter list)
construct particularly useful., as I hope to show. Below follows a commented listing on a filename parsing utility for S*BASIC that hopefully illustrates the technique:
1 rem --------------------- cut here ---------------------| rem rem To remove these remarks, and test the program: rem Cut between - - cut here - - lines (SBASIC needs the rem first item to be a line number!) and save to, say: rem C:\temp\FnmParse.txt rem LOAD FnmParse.txt into S*BASIC and then rem SAVE as say, win1_temp_FileNameParser_bas 1 PRINT,'(Simplyfied) Filename Parser' 2 rem ©PWitte 1998 3 PRINT,!!!!!'PD - No Warranties'!!!!! 4 : 5 dfnm$='win1_bas_util_fnm_ParseFnm_bas' 6 er=ParseFnm(dfnm$,ddev$,ddir$,dnm$,dext$) 7 PRINT\\'Fnm:'!dfnm$\\'Dev:'!ddev$\'Dir:'!ddir$\ 8 PRINT 'Nme:'!dnm$\'xt:'!dext$\'Err:'!er 9 STOP 10 : rem Above. The first part of the program gives some rem information, and shows and example of usage. 32724 DEFine FuNction ParseFnm(f$,v$,d$,n$,e$) rem This part of the program is the object of the rem enterprise; the file name parser itself. Due to the rem nature of the QL's file system (FS), it is impossible to rem determine how much of the latter part of the name is rem filename and how much is directoryname merely by rem inspecting the filespec. You have to actually open the rem file (or its directory) to find out. The function does rem this, and then breaks up the filespec according to a rem mixture of known facts and assumptions (ie, it's not rem foolproof!) It puts the different sections into the rem supplied variables, and returns ok. rem The function is defined as a floating point function, rem even though its main task is to manipulate, and you rem might say, return text. rem In this simplified version any values pre-supplied in rem v$..e$ are overwritten. The only parameter you should rem supply is the f$ (for Full Filespec) This is (more or rem less) expected to be in the form: rem key: <> = name; | = or; [] = optional (0..1); rem {} = repeated (0..) rem rem <sep> = directory separator, '_' rem <esep>= extension separator, '_ | .' (SMSQ/E) rem rem <filespec> = rem <device name><drive number (1..8)><sep> rem (<directory section><sep>} rem [<filename>[<esep>[<extension>]]] 32725 LOCal c,t,p%,i% 32726 rem Split filename into components 32727 c=FOP_DIR(f$):IF c<0:RETurn c rem FOP_DIR is a function (introduced in Toolkit I/II (tk2), rem by Tony Tebby, and included with many disk interfaces, rem and in SMSQ*). It tries to make the best of the rem information supplied, and will open the first directory rem that matches the first part of the filespec. So if you rem have a directory called 'win1_asm_' (but none called rem 'win1_asm_prg_...') and you did a rem rem ERT FOP_DIR(win1_asm_prg_temp) the function would open rem rem directory 'win1_asm_' taking the rest of the filespec to rem be a filename! 32728 d$=FNAME$(#c):CLOSE#c rem FNAME$ (also a function from tk2) returns the name of rem any file, also directory files. So, continuing our rem specific example above, d$ (for Directory) would now rem contain 'asm' - Note the device name is not returned. 32729 IF LEN(d$) THEN rem FNAME$ did return (at least the first) part of the rem directory name, eg 'asm'. 32730 p%=d$ INSTR f$:IF p%=0:RETurn -7 rem If the filename returned by FNAME$ is, after all, not in rem the filespec return the error Not Found. rem rem (This would be the case if you tried to: rem DATA_USE 'win1_asm' rem ERT FOP_DIR(#3;'abc_test') rem FNAME$(#3) would then return 'asm') 32731 d$=d$&'_' rem If d$ _is_ a substring of filespec, append the filename rem separator (as the last one is not stored in the rem directory file). 32732 ELSE rem At this point d$ is ''. This could mean that <root> had rem been specified; that no matching directory was found (eg rem had we specified 'win1_prog_temp_..' and there was no rem 'win1_prog_..'); or that something was wrong. 32733 p%=('_' INSTR f$)+1 rem Do a primitive test on the filespec to see if it rem contains a devicename, eg 'win1_..'. 32734 END IF 32735 v$=REMV$(p%,LEN(f$),f$) rem v$ stands for deVicename. v$ gets set to the first part rem of filespec, up to the first underscore. 32736 IF LEN(v$)<3:RETurn -12 rem Better would have been: rem 32735 IF p%<3 OR p%>5:RETurn -12 rem This version of the filename parser doesn't support rem networked drives, so: rem 32735 IF p%<>5:RETurn -12 rem would be correct here. Then: rem 32736 v$=REMV$(p%,LEN(f$),f$) rem Tests whether the first part of the filespec is a rem possible devicename. (Devicenames can only legally be 3, rem 4, or 5 charcters long, as in: rem 'S7_', 'n63_', 'ram2_'. Anything other is an error. rem Further tests should be done here to determine whether rem v$ is 'legal' device name, but there is no easy way of rem knowing for sure. (Try: OPEN_NEW#3;'flp7_test':PRINT rem FNAME$(#3) and see what you get (presuming you don't rem have an flp7_ ;) 32737 IF p%+LEN(d$)=LEN(f$) THEN 32738 n$='':e$='' rem We allow filespec to be incomplete from devicename down. rem In the case above filespec == device name & directory rem name, ie there is no filename and no extension. 32739 ELSE 32740 n$=REMV$(1,p%+LEN(d$)-1,f$) rem There is a name (and possibly an extension). Let n$ (for rem fileName) hold it for now. 32741 p%=0 32742 FOR i%=LEN(n$) TO 1 STEP -1 32743 IF n$(i%) INSTR '_.':p%=i%:EXIT i% 32744 END FOR i% rem Here we just check filename from the end of the string rem for the first '.' or '_' it encounters. This, it rem decides, will be the extension. 32745 IF p%=0 THEN 32746 e$='' rem No extension found 32747 ELSE 32748 e$=REMV$(0,p%-1,n$) 32749 n$=REMV$(p%,99,n$) rem Slice filename into name part and extension part 32750 END IF 32751 END IF 32752 RETurn 0 rem Return OK 32753 END DEFine 32754 : rem The final part of the program is a help-function REMOVE$ rem (shortened to REMV$ in its S*BASIC incarnation) all it rem does is to simplify string slicing by encapsulating all rem the error checking. It won't be looked at here. 32755 DEFine FuNction REMV$(fr%,to%,str$) 32756 IF fr% < 2 THEN 32757 IF to% >= LEN(str$):RETurn '' 32758 RETurn str$(to% + 1 TO LEN(str$)) 32759 END IF 32760 IF to% >= LEN(str$) THEN 32761 RETurn str$(1 TO fr% - 1) 32762 ELSE 32763 RETurn str$(1 TO fr% - 1) & str$(to% + 1 TO LEN(str$)) 32764 END IF 32765 END DEFine 32766 : rem The weird numbering scheme is to enable the function to rem be easily MERGEd into a larger program that needs it; rem linenumbers <100 can be removed after testing. rem ---------------------- cut here ----------------------|
Also arrays are passed by reference; when you supply an array parameter you are allowing the procedure to access your actual array. The same rules described above regarding type coercion also apply to arrays.
Unfortunately, S*BASIC provides only limited "mass" operations (for lack of a better term) on arrays though you can pretty much slice them up any which way you choose. This comes in handy if you want to write your own mass-ops in S*BASIC or machine code.
You can't do a = b with arrays in S*BASIC but you can write your own EQU a TO b, which does exactly the same (see commented listing of EQU below).
That's it, folks!
1 rem <- - - - - - - - - cut here - - - - - - - - - -> mer rem To remove these remarks, and test the program: rem Cut between - - cut here - - lines (SBASIC needs the rem first item to be a line number!) and save to, say: rem C:\temp\EQU.txt rem LOAD EQU.txt into S*BASIC and then rem SAVE as say, win1_temp_EQU_bas 1 DIM a$(2,2,2,2,6),b$(2,2,2,2,8) 2 DIM a(2,2,2,2),b(2,2,2,2) 3 FOR i%=0 TO 2 4 FOR j%=0 TO 2 5 FOR k%=0 TO 2 6 FOR l%=0 TO 2:a$(i%,j%,k%,l%)='L'&i%&j%&k%&l% 7 END FOR k% 8 END FOR j% 9 END FOR i% 10 count%=0 11 FOR i%=0 TO 2 12 FOR j%=0 TO 2 13 FOR k%=0 TO 2 14 FOR l%=0 TO 2:a(i%,j%,k%,l%)=count%:count%=count%+1 15 END FOR k% 16 END FOR j% 17 END FOR i% 18 : rem Above. Initialise a few test arrays (use plenty of rem dimensions :) Note you'll have to modify all integer rem FOR-loops to make this program run under plain QL rem SuperBASIC! 50 CLS:PRINT a$,\ 52 er=EQU(b$ TO a$) 54 CLS#0:CLS#2:PRINT#2;b$,\:PRINT#0;er 56 BEEP 2000,20:PAUSE 58 CLS:PRINT !a!\ 60 er=EQU(b TO a) 62 CLS#2:PRINT#2!b!\:PRINT#0;er\ 64 BEEP 2000,20 66 : rem Above: Test harness. Edit to taste. 100 rem EQU SBASIC function to 101 rem EQUate two arrays of the 102 rem same dimensions and type 103 rem Requires tk2 or equivalent 104 : 105 rem ©PWitte, August 1998 106 rem For "educational" purposes only 107 rem Use at own risk. No warranties! 108 : rem Can't say you haven't been warned! 1000 DEFine FuNction EQU(a,b) rem The idea is to equate one array with another in a rem reasonably rational manner, while demonstrating some of rem the niceties of parameter passing techniques using rem arrays, at the same time. rem The first thing to note is that EQU will handle any type rem of array ie, interger, sting, & float - although the rem parameter list only shows float! Also, any number of rem dimensions are handled. The only provision, in this rem implementation, is that they are of the same type and rem have the same number and size of dimensions (except rem string, in the last dimension). 1010 LOCal er rem This nice little feature of "inheritance" is not rem documented anywhere, as far as I know: rem A LOCal variable defined in one procedure will remain rem local in any procedure called by that procedure, unless rem the variable has been "re-defined" by a subsequent use rem of LOCal. rem Here the local variable, er (error flag) is set in EQU, rem the calling function, and modified in EQN/EQS. Yet a rem variable er, defined in the initial code, outside any rem procedure body, would retain its original value. The rem only danger lies in that if the same sub-routines were rem to be reused by another procedure, you may forget to rem declare it as LOCal in the calling procedure and end up rem mysteriously modifying a GLOBal variable instead! rem It certainly saves the the repeated overhead of stacking rem a local variable for each recursive call to EQN/EQS rem here, as would be the case if we defined er both in EQU rem and in EQN/EQS. 1020 IF PARTYP(a)<>PARTYP(b):RETurn -15 1030 IF PARUSE(a)<>PARUSE(b):RETurn -17 rem Checks whether paramerters are arrays, and of the same rem type. The error checking here is not foolproof. 1040 er=0 1050 IF PARTYP(a)=1 THEN 1060 RETurn EQS(a,b) 1070 ELSE 1080 RETurn EQN(a,b) 1090 END IF rem String arrays must be handled slightly differently to rem numeric ones, in that the last dimension is the string rem itself. It might be possible to find a universal rem algorithm, to handle numbers and strings, but it makes rem sense to use the built-in mass assignment features and rem copy whole strings at once, rather than byte by byte. So rem the string and numeric sides have been implented as rem separate functions. Another advandage is that this rem offers the opportunity to optimise them for their rem different uses. 1100 END DEFine 1110 : 2000 DEFine FuNction EQN(a,b) 2010 LOCal i% rem Another reason for separating out these sub-routines as rem functions, is that we can take advantage of the rem interpreter's excellent array slicing abilities, as in rem line 2070. 2020 IF DIMN(a)<>DIMN(b):RETurn -4 rem Every dimension has to match in size. This test will be rem performed before any processing takes place. The rem alternative would be to use a special sub-routine. 2030 IF DIMN(a(0))=0 THEN rem If the next dimension is past the last, then this is the rem dimension we can work with: 2040 FOR i%=0 TO DIMN(a):a(i%)=b(i%) rem Copy this dimension from b to a, element by element. rem This also terminates recursion at this level. 2050 ELSE 2060 FOR i%=0 TO DIMN(a) 2070 er=EQN(a(i%),b(i%)):IF er<0:EXIT i% 2080 END FOR i% rem More than anything, a function like EQU wants speed, so rem loops have been specialised, reducing the overheads of rem test & branch. The use of recursion is almost necessary, rem but since most arrays will be of between 1 to 3 rem dimensions, and ditto levels of recursion, it shouldn't rem significanly affect the routine's resource demands. 2090 END IF 2100 RETurn er rem The error-checking stuff is not strictly necessary in a rem programming toolkit - error-checking is performed at the rem program level - but it's easier to delete it than add it rem when needed (eg during program development). 2110 END DEFine 2120 : 3000 DEFine FuNction EQS(a,b) rem Pretty much the same as for EQN above, but optimised for rem string operations. 3010 LOCal i% rem Note that I can not use the same technique for i% as for rem er. i%'s value will be different at different levels of rem recursion ie, i% has to be saved between levels. 3020 IF DIMN(a)<>DIMN(b):RETurn -4 rem This arrangement was actually a bug, as no comparison is rem made on the last dimension. However, as the interpreter rem doesn't complain and simply ignores any supernumary rem characters, I thought I'd leave it there as a "feature". rem Ie, DIM a$(5,10),b$(5,8): er=EQU(b$,a$) rem works, though any strings longer than eight characters rem will be truncated. 3030 IF DIMN(a(0,0))=0 THEN rem Remember a() refers to an array of type string! rem Operations can be performed at a higher structural rem level, so we terminate recursion one level up. 3040 FOR i%=0 TO DIMN(a):a(i%)=b(i%) rem Copy one whole string at a time. 3050 ELSE 3060 FOR i%=0 TO DIMN(a) 3070 er=EQS(a(i%),b(i%)):IF er<0:EXIT i% 3080 END FOR i% 3090 END IF 3100 RETurn er 3110 END DEFine 3120 : rem Genuine bug/incompatibility reports and comments welcome rem Send to pjwitte at googlemail dot c0m rem <- - - - - - - - - - cut here - - - - - - - - - - -> mer