Skip to main content

Function

In Rell, functions streamline repetitive tasks by encapsulating reusable logic, such as validation or data retrieval.

  • Functions can return various data types, including custom entities and objects. Without an explicit return type declaration, a function implicitly returns unit (no value).
  • When called within operations, functions can modify the database, enabling dynamic and interactive applications.
  • Functions can be invoked from queries, operations, and other functions, promoting code organization and modularity.

Short form (concise expressions):

function f(x: integer): integer = x * x;

Full form (multi-line logic):

function f(x: integer): integer {
return x * x;
}

Return types:

  • Explicitly declared: Use a colon and the desired type name to clearly define the expected return value.
  • Implicit unit: If no return type is specified, the function implicitly returns unit.
function f(x: integer) {
print(x);
}

Default parameter values

Streamline function calls by providing default values for certain parameters. The default parameters get used if you don't specify the parameters in the function call.

function f(user: text = 'Bob', score: integer = 123) {...}
...
f(); // means f('Bob', 123)
f('Alice'); // means f('Alice', 123)
f(score=456); // means f('Bob', 456)

Named function arguments

Improve readability and maintainability by explicitly naming arguments during function calls.

function f(x: integer, y: text) {}
...
f(x = 123, y = 'Hello');

Using a function as a value

Pass functions as arguments to other functions, enabling powerful abstractions and code reuse. If you want to pass a function to another function, then you can use the function as a value by using the following syntax:

() -> boolean
(integer) -> text
(byte_array, decimal) -> integer

Within the parentheses, you specify the function input type of the passed function after the arrow follows the function's return type.

An example could look like this:

function filter(values: list<integer>, predicate: (integer) -> boolean): list<integer> {
return values @* { predicate($) };
}

Partial function application

Create new functions by pre-filling some arguments of an existing function, promoting code flexibility. You can use the wildcard symbol * to create a reference to a function (to obtain a value of a function).

function multiply(x: integer, y: integer) = x * y;

val double = multiply(2, *);
// Type of "double" is (integer) -> integer
double(456); // Invocation of multiply(2, 456) via "g"

Extendable functions

You can declare a function as extendable by adding @extendable before the function declaration. You can define an arbitrary number of extensions for an extendable function by expressing @extend before the function declaration.

In the example below, function f is a base function, and functions g and h are extension functions.

@extendable function f(x: integer) {
print('f', x);
}

@extend(f) function g(x: integer) {
print('g', x);
}

@extend(f) function h(x: integer) {
print('h', x);
}

When you call the base function, all its extension functions get executed, and the base function itself gets executed. However, extendable functions support a limited set of return types, and this behavior depends on the return type.

Return type-specific behavior

  • Unit: All extensions and the base function execute unconditionally.
  • Boolean: Extensions execute until one returns true; the base function executes only if all extensions return false. The last executed function's result is returned.
  • Optional (T?): Similar to Boolean, but extensions execute until one returns a non-null value.
  • List (list<T>): All extensions and the base function execute, and their lists are concatenated in the returned result.
  • Map (map<K, V>): Similar to List, but maps are combined into a union, failing if key conflicts arise.