Functions¶
Functions are a structure that can be called in code, may optionally take one or more arguments, and returns a value whose type must match the function declaration, unless the function is declared void in which case nothing is returned.
The purpose of a function is to give structure to code and to containerize code that might be called repeatedly. In general, almost all code must be contained inside functions. The exceptions to this are pre-processor directives, inheritance statements, global variable declarations, and function prototypes.
Variables inside functions are strictly limited by their Variable Scope.
It is important that a function that returns a value does so matching the return type specified in its declaration or you will throw an error for an ‘Unexpected type’. Simply, if you say you’re going to return an integer and instead return a float, the driver is not going to be happy.
Declaring Functions¶
Functions take the following declarative form:
//For functions with no arguments that return a value.
<modifier> <type> <function_name>()
{
<code>
return <value>; //Must be of <type>
}
//For functions with argument(s) that return a value.
<modifier> <type> <function_name>(<arg type> <arg>, <arg type2> <arg2>, ...)
{
<code>
return <value>; //Must be of <type>
}
//If a function is void then it returns nothing so the return is
//a little different; and technically optional but it's good
//practice to include the return to be explicit.
<modifier> void <function_name>()
{
<code>
return; //Void so we literally return nothing.
}
<modifier> is one or more special keywords that limit the functions scope, allow it to accept variable numbers of arguments, or limit its masking. See Modifiers for more details.
<type> must be a valid type as listed in types.
Calling Functions¶
To call a function that is in the same code file (or a file that is included with the #include preprocessor directive), you simply type the function name and any arguments:
my_function(args);
To call a function inside another object, you need to use either call_other() or the -> operator.
some_other_obj->its_function(args);
//Or...
call_other(some_other_obj,"its_function",args);
If we are using the #pragma strict_types preprocessor directive, then we need to also type-cast any calls to other objects as such:
string s;
//Presuming its_function() is defined to return a string type...
s = (string)some_other_obj->its_function(args);
//Or...
s = (string)call_other(some_other_obj,"its_function",args);
A fun feature of the 64-bit driver is that we can ‘flatten’ arrays when passed as an argument. What this means is you pass an array as an argument, if you put ‘…’ after it, rather than sending the array as a single big argument, it will send each element as a separate argument.
For instance:
//Given the following array...
string *s = ({ "a", "b", "c" });
//The 'normal' way:
void my_func(string *s)
{
string a,b,c;
a = s[0] + "!";
b = s[1] + "!";
c = s[2] + "!";
return;
}
my_func(s);
Result: a == "a!", b == "b!", c == "c!"
//Using array flattening.
void my_func2(string a, string b, string c)
{
a += "!";
b += "!";
c += "!";
return;
}
my_func(s...);
Result: a == "a!", b == "b!", c == "c!"
Prototypes¶
When the driver loads a code file, it does so linearly, from top to bottom. First it processes any pre-processor directives, then it essentially goes down the file in order plopping things in memory as it needs to. It isn’t actually executing any code at this time, its just preparing it.
A problem can arise if you try to call a function that hasn’t been defined yet. This will break:
status main()
{
do_something();
return 1;
}
void do_something()
{
//<code>;
return;
}
To get around this, we have two options:
First, We can define the function before the function that calls it. Sometimes this is fine, but sometimes it can make the code look disorganized.
void do_something()
{
//<code>;
return;
}
status main()
{
do_something();
return 1;
}
Second, we can use a prototype to tell the driver there will eventually be a function with a given type, name, and argument(s) if applicable, its a single line and is often easy to put a bunch of them towards the top of the file.
//This is the prototype. Notice it has no {} to define the actual code
//and ends in a semi-colon.
void do_something();
status main()
{
do_something();
return 1;
}
void do_something()
{
//<code>;
return;
}
Since the driver works in a linear fashion, it sees the prototype and allocates some memory for it, then when it comes across the call in main() it knows that it’s legitimate and doesn’t throw an error.
Another purpose of declaring prototypes is when you are writing a file that you intend for something else to inherit later on, you might require that future file to override a specific function, but you don’t want it to do anything in the inheritable itself. By prototyping it, you can still use it in all your code as if it existed, and rely on the person writing the file that inherits it to flesh the function out appropriately (presuming they know that because you properly documented it somewhere!).
Recursive Functions¶
Recursive functions are functions that call themselves. This is common when a function wants to process things in chunks, the first call can have the function break it into chunks, then it calls itself with each chunk to break it down further. It can also be used when the final result is depdenent on results it gets in intermediary steps.
A good example of this is a factorial function to calculate the factorial of an integer.
int factorial(int x)
{
//Can't factorial a negative number.
if(x < 0) return 0;
//Factorial of 1 or 0 is 1.
if(x == 1 || !x) return 1;
//Otherwise return the value times the value one less than itself.
return x * factorial(x-1);
}
This function will essentially loop.
factorial(4) will return 4 * factorial(3) factorial(3) returns 3 * factorial(2) factorial(2) returns 2 * factorial(1) factorial(1) returns 1
So if you then substitute the function call with the result over and over you’ll see the function ends up doing 4 * 3 * 2 * 1 and you get the correct answer, 24.
Warning
Recursive functions can be powerful, but they can also lead to a system crash. Notice in the factorial eventually the argument, due to the x-1 bit, will reach 1 or 0. If a recursive function doesn’t have a way to guarantee it will eventually end, you will have an infinite recursion, a series of function calls that never end, but will consume resources until the system crashes. This is BAD! There are safety checks built into the driver but they are not fool-proof and do not catch everything that could go wrong, so be careful!