First Steps in Programming RISC OS Computers
A RISCWorld reprint of the updated second edition
Chapter 7 :
The programs that we've met so far have been fairly simple. They have only performed each operation once, except where it formed part of a FOR ... NEXT or REPEAT ... UNTIL loop.
It frequently happens, however, that we want to perform the same, or a similar, operation at various points in a program. Consider a program called Delay. Just look at the listing, but don't bother to type it in or run it. This is not the version that you'll find in the files.
This is a rather trivial program but it includes a sequence of instructions which is repeated three times. It asks for a number, announces that it will wait for three seconds, does so, asks for a second number, announces that it will wait for three seconds, does so, prints the sum of the two numbers, announces that it will wait for three seconds, does so, then returns control to you!
As you can see, lines 50 to 70 are identical to lines 90 to 110 and lines 130 to 150. It would be useful if we could just type these lines in once and call them each time we need them.
This is a job for a procedure. Doing it this way, the program looks like this.
The extra lines which we needed to produce our procedure mean that, in this case, we've actually ended up with one more line than we started with, but the program serves its purpose. The three lines which were repeated three times are now in a procedure which we've put at the end of the program. We give it a name which describes what it does. It starts with a DEFPROC keyword so that Basic knows where to find it, and finishes with an ENDPROC keyword.
Incidentally, you can put a space between DEF and PROC if you wish, but there must be no space between PROC and the procedure name.
Each time we want to call the procedure we use a PROC command, followed by the procedure name, as in lines 50, 70 and 90. Again, there mustn't be a space between PROC and the procedure name. When the program gets to one of these lines, it jumps to the procedure, executes it down to the ENDPROC keyword and jumps back to where the PROC command was, ready to continue.
The keyword END in line 100 tells Basic that the end of the program has been reached. If it wasn't there, it would plough straight on and execute the procedure as though it were part of the main program. We haven't needed an END keyword so far, as all our programs have finished when they ran out of lines to execute. In this program, however, the listing doesn't end where the execution of the program ends.
Passing ParametersIn the original version of this program, the three time delays were identical, so the procedure which replaced them had to do exactly the same job each time it was called. What can we do, though, if we want a different delay each time we call the procedure? We can vary its behaviour by passing a number to it, telling it how many seconds delay we want.
The numbers that we pass to procedures are called parameters and we put them in brackets after the procedure name. This is the final version of Delay, which can be found in the files.
Each time we call PROCtime_delay we pass it a number, though it could just as easily be a numeric or string variable. This number becomes the value of variable t% which is in the brackets following the procedure name in line 120. The procedure now uses this value when printing how many seconds it's going to wait and also multiplies it by 100 for counting the centi-seconds in line 150.
Local VariablesBecause t% was created in this way, it becomes a local variable, which means that it exists only within this procedure. If there is another t% somewhere else in the program, it's an entirely separate variable with a different value. This is useful because it means that you don't have to think up a new set of variable names each time you write a procedure and it reduces the risk of accidentally having two variables in the program with the same name.
A variable which is not local is known as a global variable.
Variables like t% which are introduced as parameters are treated as local variables automatically, but it's also possible to create other local variables, using the keyword LOCAL, for example:
Now let's try a new version of our program that used the mouse to draw lines on the screen. This version draws crosses and circles, so it's called Mouse_X_O.
In this program we pass two parameters to our procedures - the x and y coordinates of the mouse. These determine where the centres of the crosses or circles lie. The size of the cross depends on the value of local variable size% in PROCcross and the size of the circle on the value of radius% in PROCcircle.
Structured ProgrammingThis example shows a big advantage of using procedures and one of the great strengths of BBC Basic, namely structured programming. This basically means that the program is broken down into smaller sections, each of which does a specific job. These sections may themselves be divided into smaller ones still, as procedures can call other procedures. (In some cases, it's even possible for a procedure to call itself - this is known as recursion).
If you look at the main part of the program, in lines 10 to 140, you will see that it's easy to get an overall view of how it works. We read the mouse position and the state of the buttons in line 70. If the left-hand button is pressed, we draw a cross; if the right-hand button is pressed, we draw a circle. We know this because we've given our procedures names which indicate what they do, though we don't have to know exactly how they draw a cross or a circle to understand the main loop.
When we get down to understanding the procedures themselves, the situation is reversed. We don't have to know how the x and y coordinates were produced - only that they are passed to the procedure, which draws a cross or circle around them.
... we draw a cross ...
It's worth using procedures to break down your program in this way, even if you only call each of them from one place in the program, as we do in the Mouse_X_O program. The main part of a typical game program, for example, may look like this (without the line numbers):
The first procedure, PROCinitialise, deals with everything that has to be set up at the start of the game, and is only called once. We then enter the game's main loop. This consists of a call to PROCplay_game which contains the main body of the game itself. FNyesno is a function, about which more shortly, and the game repeats itself until FNyesno returns a value of FALSE.
PROCplay_game could well look like this:
PROCreinit would re-initialise any variables which had to be reset at the start of each game, for example the number of lives that you start with, in variable lives%. After PROCdraw_screen has redrawn the screen, we enter another loop in which we play the game once, in PROCnext_go, until we lose a life. We then reduce lives% by 1 and go back for another go until lives% reaches zero, when PROCgame_over prints the words 'Game Over' in fancy lettering on the screen and asks if we want another go. The procedure then finishes, or exits, back to the main program.
We are progressively breaking down our program into smaller units which are easier to handle. This also makes it simpler to write the program, as you can do it in sections. If you want to concentrate on writing PROCnext_go first of all, you can write a simpler version of PROCdraw_screen which will draw an elementary screen to begin with. Only when you've got PROCnext_go working well need you concentrate on getting the screen drawing right.
GOSUBs and SubroutinesSome other versions of Basic use the GOSUB command to do a similar job to procedures. This keyword is included in BBC Basic (see Appendix 1) for compatibility with other versions but it's better not to use it for several reasons. GOSUB, like GOTO, jumps to a particular line number instead of calling a piece of programming by name. There is nothing at the beginning of the section (called a subroutine) to indicate what it does or indeed that it is the first line of a subroutine, unless you use a REM statement. In addition, you cannot pass parameters or use local variables.
Structured programming using procedures and functions, on the other hand, is much easier to understand when looking at a listing. We've already seen how we can break each section down into smaller ones. You can see when looking at a listing where each section starts, by the use of DEFPROC or DEFFN and where it ends. A well-chosen name for the routine will give you a good idea of what it does, both at its start and at the point where it is called. The use of parameters and local variables, again with well-chosen names, helps in understanding its inner workings. Imagine trying to understand the workings of the main loop of the Mouse_X_O program if lines 90 and 110 just said GOSUB 160 and GOSUB 230!
FunctionsIn this program we used a function called FNyesno which, you may have noticed, was used just like a variable. This is because a function is similar to a procedure, but it returns a value. There are no prizes for guessing that, in this program, FNyesno returns TRUE if you type Y and FALSE if you type N.
Here is a program called Degrees with a simple function to convert temperatures from Celsius to Fahrenheit. To do this we have to multiply by 1.8, then add 32.
The main program simply inputs the temperature in degrees Celsius and prints a message which includes the function as if it were a variable. You will see that FNconvert has one parameter, which is how we pass the value of temp% to it. Inside the function this becomes local variable c%.
As you can see, a function begins in a similar way to a procedure, with a DEFFN keyword. The ending is very different, though. Line 140 looks as if it has a mistake in it, as it begins with an equals sign. This, in fact, signals the end of the function and also shows the value that's passed back to the main program, in this case the value of variable f%.
This function could actually have been written in two lines, like this:
RETURNing ValuesWe've just seen how we can use a function to work out one value and return it to our main program, but suppose we wish to work out two or more values at the same time. You may, for example, need to get the x and y coordinates of an object on the screen. One way would be to use two functions:
We call one function, FNx_position, passing it a number which represents the object in question and the function works out the object's x coordinate and returns it to us. Next, we obtain the y coordinate in the same way, calling FNy_position. There is nothing wrong with this method but we can do both jobs in one go by using a procedure.
In our Mouse_X_O program earlier in this section, we called two procedures, PROCcross and PROCcircle, passing them two parameters. In each case, the global variables were called x% and y% and their values were passed to local variables xpos% and ypos%, which existed only within their procedures.
Nothing actually happened to the value of xpos% or ypos% in either of the procedures but, if it had, there would have been no effect on the values of x% and y% when control was returned to the main program. The passing of values through parameters is normally a one-way affair.
We could use a procedure to do the job of several functions if we could make this value passing two-way. To do this, all we have to do is put the keyword RETURN in front of each parameter which we wish to treat in this way, following the DEFPROC command. Our earlier example might look like this:
Here we see the line which calls the procedure and the first line of the procedure definition. The values of xcoord% and ycoord% are passed to xpos% and ypos% in the usual way (though this procedure would probably ignore them). When ENDPROC is reached at the end of the procedure, the current values of xpos% and ypos% are passed back to xcoord% and ycoord%. By this means we can obtain several values at once from a procedure but we can't, of course, use it like a variable as we did in line 60 of our Celsius to Farenheight program.
Choice of Two-way ParametersA variable doesn't exist until you create it by giving it a value. We could not call PROCposition in this example if we hadn't already created monster% because it wouldn't have a value to pass to local variable char%. The other two parameters, however, are a different matter. Because xpos% and ypos% have RETURN in front of them in the procedure definition, their corresponding global variables xcoord% and ycoord% don't have to already exist when we call the procedure. In this case, a local variable will initially be given a value of zero and the global variable will be created when a value is passed back to it.
A parameter used in two-way value passing must, of course, be a simple variable. We could not, for example, type:
There is no problem about passing the value of monster%+2 to local variable char%; this may be the way our program is intended to work. If we pass 5 to xpos% and ycoord%*3 to ypos%, however, the procedure will attempt to pass the final values of xpos% and ypos% back to number 5 and expression ycoord%*3, both of which, of course, are impossible.
Function and Procedure LibrariesYou can store frequently-used functions and procedures in library files and call them from your program. This is outside the scope of this guide, but you will find a brief account of how to do it in Appendix 1. The keywords which handle library files are INSTALL, LIBRARY and OVERLAY.