Chapter 10. Functions

Table of Contents

1. Function Declaration
2. Application and Local Functions
3. Function Execution: the Return Instruction
4. Passing Parameters to a Function
5. Recursion

Functions have been introduced in previous chapters, using Biferno predefined functions such as print and include.

A function is a code block that is separated by the main body of a script and implements a specific operation. Functions can optionally accept comma-separated input parameters, which behave as local variables within the function. The body of a function executes operations on the input parameters, on global variables, or on variables with wider scope. The result of these operations can be returned to the calling code.

Functions are a fundamental tool of programming languages. They allow to subdivide application code in blocks of elementary instructions that implement very specific tasks. This modularization of the code allows to write programs that are more efficient and more readily modifiable.

This chapter describes in detail how to declare new functions in a Biferno script, how to call them, how to pass them parameters and obtain results. All remarks concerning user defined functions hold as well for predefined functions.

1. Function Declaration

The declaration of a function has the following syntax:

function [class] function_name (parameter list)
{
	code block
}
    

The curly brackets are always mandatory for functions and they must be used even when the function body consists of a single line of code.

If the function returns a result, the class of the result must be always specified after the function keyword. When a function does not return any result, the class identifier can be omitted or the keyword void can be used, which indicates the absence of a value.

A function can have no input parameters. In this case the parameter list can be omitted or the void keyword can be used. In any case a pair of matching parentheses must be used after the function name upon invocation.

A function can be declared anywhere in a Biferno script. A function can be used only after it has been declared.

2. Application and Local Functions

Functions can be either of the local or of the application kind. Functions declared in the Biferno.config.bfr file (or in included files) are always of the application kind, other functions are of the local kind. application functions are visible from all scripts of the current application, local functions only from the script where the function is declared.

A function can be explicitly declared as local or application using the corresponding keyword in front of the function name in the declaration statement. E.g.:

local function void MyFunc(void)

is a local function, while:

application function void MyFunc(void)
    

is an application function.

The following rules apply:

  • application functions can be declared only in the Biferno.config.bfr file or in files included from this file.

  • Functions declared in the Biferno.config.bfr file are always application functions unless explicitly specified otherwise.

  • An application function cannot be declared if a local function with the same name exists.

application functions are in principle more efficient than local functions because they are loaded in memory only once, while local functions are reloaded every time a script is run. However, if the biferno cache is active (see Chapter 22, Cache Management), the loading of functions after the first invocation is partially optimized.

Where should application functions be declared during debugging?

We have seen how application functions have to be declared in the Biferno.config.bfr file. Debugging functions declared in Biferno.config.bfr can be cumbersome, because the application must be reloaded every time a function is modified.

A good strategy is therefore to include the function scripts directly in the various application scripts during the development phase (e.g. using the HEADER parameter) and to move the include instructions to the application configuration file when development and testing is finished. Using this technique one works on local functions which are made application when most of the debugging is done.

The considerations of this section apply also to user classes, to be described in Chapter 11, User classes.

3. Function Execution: the Return Instruction

A function is invoked from a Biferno script using its name, which must be a unique identifier, followed by a pair of parentheses containing the optional parameter list. If the function returns an output parameter, its value can be assigned to a variable of the same class. The simplest way to return a parameter from a function is by using the return instruction.

When a function is called, the function body is executed up to and including its last line, then execution resumes from the instruction following the function invocation in the main script, unless the function body contains the return instruction. If this is the case, the execution of the return instruction is the last instruction executed in the function and control is returned to the main script. If the return instruction is followed by an expression, the result of the expression is calculated and the result is returned to the calling script.

The function in the following simple script returns the sum of two integer numbers:

<?
	function int sum (int a, int b)
	{
		return a + b
	}

	a = 1
	b = 2
	c = sum(a, b)
?>
    

A function can contain more than one return instruction, but beware that multiple return instructions reduce code readability. An example is the following script that contains a function which returns the value of the sum of two integers or a limit maximum value if the sum is greater than the limit value.

<?
	function int sum_max (int a, int b, int max)
	{
		if (a + b > max)
			return max
		return a + b
	}
?>
    

The same function can be also written as follows using a single return instruction:

<?
	function int sum_max (int a, int b, int max)
	{
		s = a + b
		if (s > max)
			s = max
		return s
	}
?>
    

The second example uses a temporary variable, s, to store the sum of a and b. This variable is local to the sum_max function and is not visible outside the function. Remember that global variables or variables having as scope the entire application (application, session and persistent) are accessible from within a function, but their scope identifier must be explicitly specified before the variable name.

4. Passing Parameters to a Function

The syntax for the parameter list of a function is the following:

class [nonames] [*]parameter_1[=expression_1], 
class [*]parameter_2[=expression_2], ..., 
class [*]parameter_n[...][=expression_n]
    

The elements between square brackets represent optional elements.

Parameters are separated by commas and must be preceded by the identifier of the class they belong to. Optionally, a default value can be specified for parameters by following the parameter name with the assignment operator =, followed by an expression (normally a constant value). Notice that actually, since a function can have no parameters, all function parameters are optional. If no default values are specified, some predefined defaults are used anyway. They are 0 (zero) for numerical parameters, false for Boolean parameters and "" (empty string) for string class parameters. For other, non-primitive classes, an uninitialized object of the required class is passed. It is possible to determine if an object has been initialized by using the static method:

boolean object.IsInitialized(obj variable)
    

Notice that variables of primitive classes are always initialized (to either 0, or "", or false).

For other classes, next to the one case discussed here, there is only another case in which a variable can be uninitialized. We will see that in the context of the declaration of new classes in Chapter 11, User classes.

If we pass to a function upon invocation less parameters than the number declared in the function prototype, Biferno supplies default values for missing parameters, as explained above. If we pass too many parameters to a function upon invocation (more than the number declared in the function prototype), the Err_PrototypeMismatch error is generated.

It is possible to define a function that accepts a variable number of parameters, even greater than the number implied by its prototype, by using an ellipsis () after the name of the last parameter in the parameter list. The ellipsis indicates that an arbitrary number of parameters may follow. A function that will accept zero or more input parameters can be defined with a prototype such as function myFunc(…). Within the function the number and values of the parameters actually passed can be discovered by using the Biferno predefined methods curScript.GetTotVariables and curScript.GetIndVariables. An example is:

<?
	function array FillArray (...)
	{
		tot_val = curScript.GetTotVariables()
		arr_obj = array()
		for (i = 1; i <= tot_val; i++)
			{
				val = curScript.GetIndVariable(i)
				arr_obj.Add(val)
			}

		return arr_obj
	}

	myArray = FillArray(12, 3, 4, 8)
	// myArray is array(12, 3, 4, 8)
?>
    

The FillArray function fills an array with the supplied values. If we would not have used an ellipsis in the function prototype an Err_PrototypeMismatch would have been generated, because the number of parameters passed would have been greater than the number of parameters declared in the prototype.

We mentioned that, when a function is called, some or all parameters can be omitted, and care must be taken not to alter the correspondence between values passed and the order of parameters in the list. More flexibility can be gained by specifying directly the correspondence between values passed and parameters using the syntax "parameter_name:value". In this way the order of parameters can be altered, as in the following example:

<?
	function array FillArray2 (obj val1, obj val2)
	{
		arr_obj = array()
		arr_obj.Add(val1)
		arr_obj.Add(val2)

		return arr_obj
	}

	myArray = FillArray2("val2":12, "val1":3)
	// myArray is array(3, 12)
?>
    

The FillArray2 function fills an array with the supplied values. Notice that in the function call the parameter order is inverted and the name of each parameter is specified. If the nonames clause is specified at the beginning of the parameter list, the names associated to the values passed to the function are not taken into consideration. The aforementioned mechanism is disabled and only the position of the value in the list counts. Sometimes it is necessary to use the string names associated to the passed values for other purposes, as in the Add and Insert methods of the array class, which use the names as the names of the new element of the associative array.

This example uses the concepts explained above:

<?
	function array FillArray3 (nonames obj valN...)
	{
		tot_val = curScript.GetTotVariables()
		arr_obj = array()
		for (i = 1; i <= tot_val; i++)
			{
				val = curScript.GetIndVariable(i, &name)
				arr_obj.Add(name:val)
			}

		return arr_obj
	}

	myArray = FillArray3("val1":12, "val2":3, "val3":4, "val4":8)
	// myArray is array(val1:12, val2:3, val3:4, val4:8)
?>
    

The FillArray3 function accepts an arbitrary number of parameters and creates an array with as many elements, assigning to the indices the names associated to the parameters. The curScript.GetTotVariables method returns the number of variables currently defined (the default is to count only local variables). If called on the first line of code in the body of a function, curScript.GetTotVariables returns the number of parameters actually passed to the funtion. The curScript.GetIndVariables method returns the value of the variable corresponding to a given position in the list and, optionally, the name of the variable itself for a variable passed by reference (name). The name returned for variables corresponding to function parameters is the name assigned to the parameter in the prototype of the function itself. In our case, having used the nonames clause, the string in front of the parameter in the function invocation is returned.

In the examples above, function parameters have been always passed by value. When this is the case, the function actually receives a copy of the original variables within the calling script. The value of the original variables can not be modified by the execution of function code. Passing parameters by value is the most common way to provide information to a function.

Biferno supports passing parameters to a function by reference. This is similar to the C language, even though pointers do not exist in Biferno. A parameter passed by reference must correspond to a variable (local or of other scope, and possibly uninitialized) of the calling script. If the value of a parameter passed by reference is modified, the original variable is modified and takes the same value.

Parameters are passed by reference by using the * character (star) before the parameter name in the function parameter list and the & character (ampersand) before the name of the corresponding variable in the function call. Within the function no special characters need to be used to identify variables passed by reference. Passing parameters by reference is an alternative way of obtaining return values from a function.

The remarks we made in the case of functions apply also when parameters are passed to the methods of a class.

Passing file and db class parameters

Care must me taken when file or db class parameters are passed, because objects of these classes are connected to resources that are external to Biferno. In these cases, when the object passed by value is "cloned" within the function (or method), also the connection to the external resource (file or database) is duplicated. This poses a risk of opening the same file multiple times or to unnecessarily increase the number of open connections to the same database, which is at best a waste of resources and may have unforeseen consequences.

We advise to always pass parameters by reference when using file or db class objects as parameters.

It is possible to inquire if a class called className "clones" its objects (as the file and database classes do) by examining the value of the property:

	classInfo(className).cloneIsNeeded
     

Array type parameters

In the prototype of a Biferno function it is possible to declare parameters that are arrays of elements of a given class. Since the class a parameter belongs to is always specified in a prototype, it is advisable to use a syntax which is different from the one we have seen for the declaration of array class variables. If we write:

int nome[]
     

we have declared an int class parameters which is a one dimensional array of integers. To declare multidimensional arrays we specify a pair of square parentheses for each dimension of the array.

Array class parameters can also be declared. In this case the element class is determined by the first assignment of a value to an element of the array itself.

5. Recursion

A function can call other functions from within its body, and can even call itself. A function that calls itself is called a “recursive” function.

Recursion is an elegant programming technique that suits the implementation of inherently recursive algorithms. Often the same problem can be solved by recursion or by iteration.

A recursive function must be structured in such a way that infinite recursion is avoided.

A simple problem that can be solved both by iteration and by recursion is the computation of the factorial of a number. The factorial (symbol: n!) of a positive integer is defined by the following expression: n! = n * (n - 1) * (n - 2) * ... * 2 * 1. Notice that 0! = 1 and 1! = 1 by definition.

An iterative function to compute factorials can be simply written as follows:

<?
	function int factorial (int n)
	{
		if (n < 0)
			return -1
		else if (n == 0 || n == 1)
			return 1
		else
			{
				fact = n
				for (i = 1; i < n; i++)
					fact *= n - i
				return fact
			}
	}
?>
    

The factorial of an integer number can also be defined as the product of the number by the factorial of the integer immediately preceding it, i.e. as n! = n * (n - 1)!. This definition leads to the implementation of the recursive function:

<?
	function int factorial (int n)
	{
		if (n < 0)
			return -1
		else if (n == 0 || n == 1)
			return 1
		else
			return n * factorial (n - 1)
	}
?>
    

This solution is more elegant and seems more efficient.

However, it should be noted that recursive functions can require a large amount of memory for local variables (stack memory). To handle such situations Biferno is forced (at least on certain platforms) to allocate sizeable memory resources, and this may slow down the execution of the script. We advise to evaluate on a case-by-case basis the use of recursive functions and to compare, whenever possible, the execution time of a recursive function versus the execution time of its iterative version.