Lesson 6: Subroutines and Functions
To program efficiently you need the ability to reuse code. This not only saves RAM but time coding as well. Furthermore the ability to save these reused snippets of code in a library allows the programmer to use them in multiple projects. This is where subroutines and functions come into play allowing you to create code that can be reused as many times as you wish.
A subroutine is a chunk of code within a larger program that does something specific and can be called upon from anywhere in the source code. A function is identical to a subroutine except for one important difference; a function returns a value after it has been called upon. Once called upon functions and subroutines perform the code contained within them and then return back to the point in the source code where they were called upon. Functions and subroutines can be thought of as mini-programs that can be accessed by jumping out of the main code, performing the subroutine or function, and then jumping back into the main code.
Subroutines were created for the need to reuse code. In early forms of BASIC this was accomplished with the GOSUB statement. Many programmers however avoid the use of the GOSUB statement for the same reason as the GOTO statement, it creates hard to read code. However, just like the GOTO statement we need to discuss the GOSUB statement a bit because you may run across them in older source code. Type in the following program to see the GOSUB statement in action. Save the code as Gosub.BAS when finished.
Figure 1: My gosh, it's full of stars!
Just like GOTO, GOSUB requires a label to jump to. Unlike GOTO however, when GOSUB encounters the RETURN statement it returns to the line of code that called the label and continues processing. This may seem a bit cleaner and produce less spaghetti code in the long run but it still makes code difficult to follow if used too much. Here is the code rewritten to use SUB statements instead. Type the following code into your IDE and save as Subs.BAS when finished.
The code may not look that different at first glance but there are a few key things that need pointing out. First, QB64 recognizes Directions() and DrawStar() as subroutines and color codes them accordingly in the source code, in our case green. You have basically made two new commands that can be called upon by their name anywhere in your source code. Subroutines are defined by using the SUB statement and then the END SUB statement to mark where they end.
Secondly, information can be passed into a subroutine through the use of local variables. Again, think of a subroutine as a mini-program within your program. The subroutine DrawStar() has been set up in such a way that two pieces of information must be sent along when the subroutine is called.
SUB DrawStar (x%, y%)
When you call upon DrawStar() it expects two parameters to be sent along with it. x% and y% become the local variables that hold that information. x% and y% can only be seen by the DrawStar() subroutine, meaning that they are local to that subroutine only. In line 16 of the example code you can see where _MOUSEX and _MOUSEY are passed in with the subroutine.
DrawStar _MOUSEX, _MOUSEY
_MOUSEX and _MOUSEY are two QB64 commands that report the current location of the mouse pointer within the program's window. _MOUSEX returns the x location of the mouse pointer. That number is then passed on to x% in the DrawStar() subroutine. Likewise, _MOUSEY returns the y location of the mouse pointer and that number is passed on to y% in the DrawStar() subroutine. x% and y% can now be used as standard integer variables locally within the DrawStar() subroutine.
Thirdly, the QB64 IDE keeps track of subroutines and functions allowing for quick and easy navigation to and from them. Select the "View" menu and then select "SUBs..." in the drop down menu. Figure 2 below shows the screen that will appear.
Figure 2: Viewing subroutines and functions in the QB64 IDE
From this screen you can navigate quickly to any subroutine or function that is contained within your source code. This screen will become your friend when you start developing code with many subroutines and functions to manage. I personally have written games and utilities that contain well into the 100s of functions and subroutines.
Functions behave exactly like subroutines with a few key differences. First, functions are created with the use of the FUNCTION statement and end with the END FUNCTION statement. Second, functions are used to return a value to the code that called them. To get a better idea of this let's take the Fahrenheit to Celsius converter we wrote in lesson 2 as an example.
That math equation in line 16 of the code is just begging to be converted to a function.
The Celsius!() function was written to take in a single variable type as a parameter, Temperature!, and then return a single type value within the name of the function itself. The function Celsius!() was declared using an exclamation point ( ! ) signifying that the returned value should be of the single data type. Think of functions as super variables that can change their value based on a parameter(s) passed in.
Notice also that the only variable we needed to declare with DIM is Fahrenheit!. The variable Temperature! is only used locally within the function and is automatically declared in the FUNCTION line of code. This statement in line 11:
is passing the value contained in Fahrenheit! to the Temperature! variable contained within the function. This is known as passing by reference.
You may be thinking that the new code we created using a function is actually more complicated than the original code. Well, here's where the super variable part comes in. Type in the following code and execute it.
The example code above illustrates how a function can be called over and over and the result returned based on the information passed to it. Here is another sample program that uses two functions to solve for the Pythagorean Theorem.
Note: Due to the length of this program the source code has been included in the tutorial asset file and is named Pythagoras.BAS. I highly encourage you to type the code in however. The more you type code the more natural it will feel.
Figure 3: QB64 can do your homework!
Don't let the length of this example program scare you. Take each line one at a time and you'll see that most of the code we have already covered. Also notice the generous use of remarks throughout the code explaining exactly how the program operates. Make sure to get into the habit of remarking your code very well for both your benefit and other programmers reading your source code. Having to deal with uncommented code, even your own a few months later, is a huge waste of time.
In the function SolvePythagoras!() there is a new mathematical statement called SQR(). SQR() is a BASIC function that returns the square root of a number. There is an upcoming lesson devoted to the various mathematical functions contained in QB64.
The Pythagorean Theorem is used quite often in game programming because it offers a simple method of calculating the distance between two objects and is used as a basis for collision detection. Sides A and B of the triangle can easily be calculated from the x,y coordinates of each object. From there the hypotenuse (side C) can be calculated which is the distance between the two objects. Very handy!
Local vs Global Variables
Variables created in the main body of source code are known as global variables. Global variables can be used anywhere in the main body of the source code. Global variables however can't be seen within subroutines and functions. Type the following example program into your IDE that highlights this behavior.
Figure 4: Two different values
As you can see after executing the code GlobalVariable% does not have any value when inside of MySubrotuine(). Notice also how the IDE did not capitalize globalvariable% inside of MySubroutine(). When GlobalVariable% was declared on line 5 it was made global because it was declared in the main body of code and is only recognized there. Inside of the MySubroutine() subroutine GlobalVariable% is not recognized as a declared variable. In fact, globalvariable% inside of MySubroutine() is a completely different variable all together. Even though they share the same name they are in fact seen as two different variables by the IDE. Any variable used inside of MySubroutine() is considered local to that subroutine. Just like GlobalVariable% can't be seen by MySubroutine(), globalvariable% (in all lower case) can't be seen by the main body of code. This allows the programmer to use variables with the same name inside and outside of subroutines and functions. Handled correctly this can actually be beneficial.
Shared Global Variables
You've already seen how you can pass a value by reference to a subroutine or function but what if you need both the main body of code and the subroutine/function to see GlobalVariable%? You can use the SHARED statement to do this. Make one small change to line 5 in the example code as seen below.
Figure 5: The variable and its value is now shared
Did you notice that GlobalVariable% within MySubroutine() capitalized after making the change? By adding the SHARED statement to DIM in line 5 you told the IDE that GlobalVariable% can be seen everywhere including inside subroutines and functions. This time when you execute the code the values are the same.
Using SHARED in conjunction with DIM allows a variable to be seen everywhere, in ALL subroutines and functions throughout the entire program. Constant variables declared with CONST are also seen everywhere throughout the source code without the need for the SHARED statement.
There is another SHARED method that allows only the subroutines and functions you choose to share a global variable's value. Change line 5 in the code back to the way it was and then add one small change to the subroutine as see in line 25 below.
Executing the program yields the same results as before. GlobalValue% is still shared within the subroutine however no other subroutines or functions within your program will be able to see GlobalValue% as a SHARED variable, only MySubroutine() can. Sharing a global variable within subroutines and functions that need them makes tracking down variable value bugs easier and streamlines source code for easier reading later on.
You may be thinking at this point, "Why not just DIM all variables as SHARED?" Well in truth with BASIC you can do just that. However, sharing all variables actually slows down execution speed of your program due to the fact that the program has to keep track of all variable values at all times. Also, in most other programming languages it is required to share variables only where needed or not even possible to share all variables all the time. Try to get into the habit of using the SHARED statement when writing code to distinguish between your program's local and global variables.
Passing Values and Unintended Results
As you've seen functions return values back to the calling code through the use of their name. It's also possible to change the value passed in by reference to a subroutine or function and have that change affect the original variable that was referenced. To see what is meant by this type the following code in and execute it.
Figure 6: Value changed by reference
Even though SomeVariable% was never referenced in the MySubroutine() subroutine it's value was changed by changing the local variable Value% within the subroutine. When you pass a value in by reference to a subroutine or function that value gets taken on by the variable parameter name defined in the subroutine or function line.
SUB MySubroutine (Value%)
Here, Value% took on the value stored in SomeVariable% but a link between Value% and SomeVariable% still remains. If you change the value in Value% within the subroutine or function that change is linked back to the original variable which is SomeVariable% in this case. You need to be aware of this behavior when dealing with subroutines and functions and their passed values. Most programmers will tell you that you should not use this behavior as a method to change variable values because it causes an unexpected outcome. Other programmers will tell you that it keeps in the spirit of functional or procedural programming practices to use it. Either way, if you don't pay attention crazy things can happen within your code that are difficult to track down.
One other thing to note about local variables is that they do not hold their value when a subroutine or function has been exited. In the example above Value% was set to 20 in the subroutine. However, when the program exits the subroutine, all local variables are reset to zero or null in the case of string variables. If you need a local variable to hold its value between procedure calls you need to use the STATIC statement like so:
Instead of using DIM to declare the local variable inside of a function or subroutine you would need to use the statement STATIC instead. This directs the code to retain the values of all local variables between calls to the subroutine or function. Static local variables are a common occurrence in subroutines and functions that call themselves, usually many times over, otherwise known as recursion.
If there are values that I need to pass and I want to avoid the unexpected change the first thing I do is assign the passed values to locally declared variables like the next example program demonstrates.
Figure 7: All looks good here
By declaring the local variables Num1% and Num2% in the Multiply%() function and then using those variables to convert n1! and n2! to integers, the original variables Number1! and Number2! never become affected. However, if this was not done the outcome would look very different as shown in the example code below.
Figure 8: Unexpected results
In this example Number1! and Number2! have become affected by what happened in the Multiply%() function. If Number1! and Number2! would be needed later on with their original values intact that would not be possible. Again, be aware of how passing values around can affect the outcomes of the variables involved in these situations.
Create a subroutine that draws a triangle when passed the height and base leg length of the triangle. The screen output should look like Figure 9 below.
Figure 9: A drawn triangle
- The graphics screen is 800 pixels wide by 600 pixels in height.
- A red line should be drawn vertically in the center of the triangle indicating the height provided.
- A red line should be drawn indicating the base length provided.
- Two lines in yellow should complete the overall triangle.
- Name the subroutine DrawTriangle() and pass the supplied height and leg length to it.
- Only simple addition, subtraction, and division are needed to complete this assignment.
- Save the program as Triangle.BAS when finished.