Chapter 7. Functions

Function definitions in Rax have the following form:

    output_type <- input_type: function_name <-
    {
      // Function body
      out := some_transformation_of(in);
    };
       

The first thing to note is that Rax functions have only one parameter and one result. The parameter and the result, however, can be of complex type, such as tuple or set. Therefore, one can pass an arbitrary number of parameters and return an arbitrary number of results by grouping them into a tuple.

Within the function body, there are two special variables available: in that contains the value of the parameter passed to the function and out to which the result should be assigned within the function body. Since most functions have a tuple as their input type, and many also as their output type, the with-clause is particularly handy when writing functions. Below an example function:

    [&:x, &:y]: Point;
    & <- [\Point: p1, \Point: p2]: cartesianDistance <-
    {
      in.{
        out := sqrt:((p2.x - p1.x)**2 + (p2.y - p1.y)**2);
      };
      out := out :\ 1e-4; // Floor for nice output.
    };
         

An invocation of a function looks as you would expect:

    \Point: p1 := [1.0, 2.5];
    \Point: p2 := [2.5, 1.0];
    `print cartesianDistance(p1,p2); // Output: 2.1213
         

Recursion.  Rax allows the functions to be recursive. As an example, a very inefficient implementation of a famous series:

    #<-#: FibonacciNum <-
    {
      out := in
             ? <= 0 : 0
             ? == 1 : 1
             ?      : FibonacciNum(in - 1) + FibonacciNum(in - 2);
    };

    `print FibonacciNum(9); // Output: 34
         

Note that number in Rax are limited to 64 bits, so the 92nd Fibonacci number is the largest number that can be generated this way.

Nested functions.  Unlike C, Rax supports nested functions, i.e., functions that are lexically encapsulated within other functions. The scope of the nested function is contained within the scope of the enclosing function and therefore the nested function can use local variables of the enclosing function, which are normally invisible to other functions, defined outside the enclosing function. Note that Rax uses lexical scoping. Nesting allows for a better program structure, where functions can be split into smaller functions that can be defined locally and thus do not clutter other parts of the program, to which they are not relevant. For example:

    {[#:id, &:pr_pension]} <- {[#:id, &:age, &:cur_income]}: predict <-
    {
       &: secret_coefficient := 0.6784;
       & <- [&:age, &:income]: secret_formula <-
       {
         in.{
           out := secret_coefficient * age * income;
         }
       }
       out := project[.id, secret_formula(.age, .cur_income)] .in;
    };

    {[#:id, &:age, &:income]}: incomes := ...
    {[#:id, &:pension]}: predicted_pensions := predict(incomes);
         

In this example, we defined a function predict that predicts the pensions of a group of people. Function predict defines a nested function secret_formula, which computes the predicted pension based on age, current income, and a secret coefficient. Note that secret_coefficient is defined outside the nested function. In Rax, unlike some other functional languages, functions can access non-local variables.

Higher-order functions.  Functions in Rax are first-class citizens, which means that they can be passed as arguments to other functions, returned as values from other functions and assigned to variables. This allows to define higher-order functions, i.e., functions that take other functions as parameters and/or return functions as parameters. An example of an higher-order function is shown below:

    #: C, T, K;                      // Use numbers for all.
    \C <- \T: Cipher;                // Cipher has built-in key.
    \C <- [\K: key, \T: txt]: Coder; // One step en/decoder.

    \Coder: Encode <- { out := in.txt + in.key; };
    \Coder: Decode <- { out := in.txt - in.key; };

    // mkCipher: build Cipher from coder and key.
    \Cipher <- [\Coder: coder, \K: key]: mkCipher <-
    {
      in.{
        out <- {
          out := coder(key, in);     // Apply key to in.
        };
      };
    };

    \Cipher: Single := mkCipher(Encode, 6);

    `print Single(3);    // Output: 9
         

In the example above, five (uninitialized) variables are defined. Throughout the rest of the code snippet, these variables are not used to store values but to be used in conjunction with the type-of operator: \. A variable used like this is called a type holder for obvious reasons. Of these five type holders, two are functions: Coder and Cipher. The first has the type # <- # indicating that it is a function that converts a number into a number. The second function has a slightly more complex type: it converts a \K (a key, in this case a number) and a \T (a text, in this case also a number) into a \C (a ciphertext, number). Traditionally, higher order functions start with mk. In the above example the higher order function: mkCipher, makes a \Cipher, i.e., a function. Its input arguments include a \Coder, i.e., also a function. A slightly more complex example is shown below:

    // mkTDEA: TDEA from encoder, decoder and three keys.
    \Cipher <- [\Coder: E, \Coder: D, \K: k * 3]: mkTDEA <-
    {
      in.{
        out <- {
          out := E(k#1, D(k#2, E(k#3, in)));
        };
      };
    };

    \Cipher: Keying'1 := mkTDEA(Encode, Decode, 6, 8, 3);
    \Cipher: Keying'2 := mkTDEA(Encode, Decode, 6, 8, 6);
    \Cipher: Keying'3 := mkTDEA(Encode, Decode, 6, 6, 6);

    `print Keying'1(3);  // Output: 4
    `print Keying'2(3);  // Output: 7
    `print Keying'3(3);  // Output: 9
         

The above higher-order function, mkTDEA, builds a \Cipher function from a set of \Coder functions and the three keys. Note that by replacing the type holders with more suitable ones, this code actually could be used to implement a usable version of the Triple Data Encryption Algorithm. The Encoder and Decoder functions would need some changes too.

Closures.  Since Rax functions are first-class citizens and can access non-local variables, support for closures is necessary. A closure is a reference to a function together with its environment. A closure contains references to all non-local variables used by a function, so that the function can access them even when its called from outside its immediate scope, which can happen, for example when a function is returned from another function. As an example, a modified version of our derive function:

    & <- &: Func;
    \Func <- \Func: derive <-
    {
      &: dx := 1e-8;
      \Func: f  := in;
      out <-
      {
        &:y  := f(in);
        &:dy := f(in + dx) - y;
        out      := dy/dx;
        out      := out :\ 1e-4; // Floor for nice output.
      };
    };

    \Func: my_func  <- { out := 2 * in**2 + 3 * in - 4; };
    \Func: my_func' := derive(my_func);

    `print my_func(0);  // Output: -4
    `print my_func'(0); // Output: 3
         

In this example, dx is not a function argument, but a secret variable declared within the body of function derive. However, my_func' can still access it, even though it was called from outside dx's scope.