Concept


A function is a sequence of operations or instructions that are packaged as a single unit and can be called from various points in a program. They are used to allow reusability of code and reduce duplication. In previous lessons we learned many builtin functions in Alusus which are used to deal with strings and arrays. In this lesson we will learn how to create new functions and how to use them.

Defining Functions


You can define any function in Alusus as follows:

def function_name: function (parameters) => return_type {
  // function body
}

Or with the following short form:

function function_name (parameters): return_type {
  // function body
}

Where:

  • function_name: a name to call the function with.
  • parameters: the parameters or arguments passed to the function.
  • return_type: determines the type of the data the function will return when it finished. The return type may be any data type in Alusus (Int, Float, Char, etc...). It is possible to put a user-defined class, which means that the function return an object from that class (we will see that in later lessons). In case the function does not return any value, we don't put anything as return type as shown in the next example:
  • function body: the body of the function, which means statements we put in the function.

Example 1:

In the next example we will define a function that print a specific sentence, and we will call this function "myFunction". We won't write any return type because this function only do a print operation, after that we wll call that function.

import "Srl/Console.alusus"
use Srl.Console;
function myFunction () {
  print("My first function is called");
}
myFunction();
/*
My first function is called
*/

Example 2:

In the next example we will define a function to calculate the sum of two entered numbers and return that as the result.

import "Srl/Console.alusus";
use Srl.Console;
def a: Int = 3;
def b: Int = 14;
function sum (x: Int,y: Int): Int {
  return x + y;
}
def z: Int = sum(a, b);
print("%d", z); // 17

The compiler executes any code in the global scope, i.e. outside functions (in the root scope for example) as soon as it encounters them, so functions must be defined before any global code that uses them. For example, the following code will cause an error:

func f1() {
  f2();
}
f1();
func f2() {
}

Whereas the following code is correct:

func f1() {
  f2();
}
func f2() {
}
f1();

The reason is that in the first case we called `f1` function that contains in it a call for `f2` function which is not defined yet. Whereas in the second case we avoid that by defining it first then calling `f1` function.

When calling a function, it is possible to pass arguments to the function in two ways, pass by value and pass by reference. In the previous examples we used the first way. The differences are the following:

  1. By Value: the real value is passed to the function, and the new memory allocated for the arguments passed could only be used inside the function, and we can't edit the real arguments here (because we only have a copy of it), which means any modification remains at the function scope. Note the following example:
    import "Srl/Console.alusus";
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: Int, b: Int): Int {
      a += 1;
      return a + b;
    }
    def z: Int = sum(a, b);
    print("%d\n", z); // 18
    print("%d\n", a); // 3
    // Note that the value of the original variable did not affected by the modification.
    
  2. By Reference: Instead of copying the variable, its address is passed as an argument to the function. The operator `~ptr` is used to pass the address of the variable to the function when calling it, and the modifications in the function on these arguments will change the original ones too (because they are working on the same memory).
    In the next example we will rewrite the previous example but with pass by reference, we will also change the value of `a` inside the function to see how that change is also applied outside the function.
    import "Srl/Console.alusus"
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: ptr[Int], b: ptr[Int]): Int {
      a~cnt += 1;
      return a~cnt + b~cnt;
    }
    def z: Int = sum(a~ptr, b~ptr);
    print("%d\n", z); // 18
    print("%d\n", a); // 4
    

    Note how the changes affect the original variable too, because we did not make a copy of it, instead we dealt with it directly through its memory address. You can also use references instead of pointers, as follows:

    import "Srl/Console.alusus"
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: ref[Int], b: ref[Int]): Int {
      a += 1;
      return a + b;
    }
    def z: Int = sum(a, b);
    print("%d\n", z); // 18
    print("%d\n", a); // 4
    

Variadic Functions


They are functions that can take a variable number of arguments (with unspecified types) which adds flexibility to your program. To explain the benefit let's consider a simple example. If we want to write a function that adds two integers, we can write a function like this:

function addNumbers (nNumberOne: Int, nNumberTwo: Int): Int {
  return nNumberOne + nNumberTwo;
}

And if we want to write another function that adds three integers, we can write the following function instead:

function addNumbers (nNumberOne:int, nNumberTwo:int, nNumberThree:int): int {
  return nNumberOne + nNumberTwo + nNumberThree;
}

We can continue adding more functions with the right number of arguments that we want to add, but this will be tedious and not practical. It will also be hard if we want to allow adding variables of different types (could be integer or real numbers). Alusus provides an elegant way to handle this using variadic functions.

You should have noticed that in `print` function, when we want to print a single number, we do the following:

print("the one number = %d", nOneNumber);

When we want to add two numbers we use the same function as follows:

print("the first number = %d, the second number =%d", nOneNumber, nSecondNumber);

This is because `print` function is a variadic function, which accepts any number of arguments.

Specifying the arguments as variadic arguments is done using operator `...` when defining the type of the argument. Which means preceding the type of the argument by `...` make that argument a variadic argument which allows the user to pass unspecified number of arguments from that type.
Functions with variadic arguments are defined like this:

function function_name (a:Data_type_a, arg_group_name: ...Data_type_b) { ... }

Where:

  • a: a regular argument.
  • arg_group_name: a name of a variadic arguments group.
  • Data_type_a and Data_type_b: they are the types of the first argument and the variadic arguments respectively.

Replace `Data_type_b` with the keyword `any` if the type of the variadic arguments is not specified.
It is possible to set an upper and lower bound on the number of variadic arguments, as follows:

function function_name (
  a1:Data_type ,arg_group_name: ...[Data_type, min_count, max_count]
) { ... }

Which means that the number of arguments passed to the function couldn't be less than `min_count` or greater than `max_count`.

Calling a variadic function is done in the same way as calling a regular function.

Using the variadic arguments inside the function is done using operator `~next_arg` as follows:

arg_group_name~next_arg[Data_type]

Where the operator needs the type of the argument because the definition may not specify the type of the argument (when we use `any`) which means the user must specify the type of the argument by itself using other arguments like a string that determines the structure of the arguments as it is the case with `print` function (which is similar to `printf` function in C). It is important to notice that each use of `~next_arg` will pop the next argument from the set of arguments, which means accessing the arguments is sequential and we could not access the same argument multiple times or accessing the arguments in random order. Also, specifying the number of arguments and stopping after popping the last element is the responsibility of the programmer since Alusus does not have a way to tell the number of remaining arguments, so the user needs to add the number of the arguments as the first argument to the function.

Example 1:
Now we will define a variadic function that adds unspecified number of integers.
Note that we will define a function that deals with only one known type which is `int`.

import "Srl/Console.alusus";
use Srl.Console;
function addNumbers (count:int, nNumber: ...int): int {
  def sum: int = 0; // a variable to store the sum of the integers.
  def i: int;
  for i = 0, i < count, i++ {
    sum += nNumber~next_arg[int]; // extract the items and add them to the sum.
  }
  return sum;
}
def num: int = 5;
num += addNumbers(4, 3, 2, 5, 10);
print("%d\n", num); // 25
num += addNumbers(1, 300);
print("%d\n", num); // 325

Example 2:
The next example defines a variadic function that accepts unspecified number of arguments from `int` and `String` types and print them.
Note that in the previous example the type of the arguments is known, but here the type is not specified, so we should find a way to know the type of each argument. We will use a string in a way similar to `print` function in which we use `d` for `Int`, `f` for `Float`, or `c` for `Char`. Here we will use `#` for `Int` and `$` for `String`.

Before starting we should note the following (these notes are mentioned in previous lessons):

  1. Any string is stored sequentially in the memory, and character `\0` is added to its end (it is added in the memory in a hidden way to mark its end). So if we have the following string:
    "#$#"

    Then its representation will be:

    {'#','$','#','\0'}

  2. In case we defined a pointer to a Char:
    def p: ptr[Char]
    

    And make it points to the first character in the following string:

    "#$#"

    Then added 1 to that pointer:

    p = p + 1

    Then that move forward by 1 byte in memory because the data type of the pointer is `Char`. So this will make the pointer `p` points to the second character of the string (which is `$`).
    Then if we add another 1 to the pointer, it will point to the next character which is `#`.
    Finally, if we repeat that another time, which means if we add 1 to the pointer, it will point to `\0` which is the end of the string.

Now using the previous notes we will define the required variadic function.

import "Srl/Console.alusus";
import "Srl/String.alusus";
use Srl;
function printArguments (format: ptr[Char], args: ...any) {
  // continue printing as long as we don't reach the string end.
  while format~cnt != 0 {
    if format~cnt == '#' {
      Console.print("%d\n", args~next_arg[Int]); // the argument is an `Int`.
    } else {
      Console.print("%s\n", (args~next_arg[String]).buf); // the argument is a `String`.
    }
    format = format + 1; // move forward by 1 byte.
  }
}
def str: String = "Hello";
printArguments("#$#", 5, str, 5);
/*
5
Hello
5
*/