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

You can use a wildcard symbol * to create a reference to a function, that is, to obtain a value of a function type that allows you to call the function:

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

val g = f(*); // Type of "g" is (integer, integer) -> integer
g(123, 456); // Invocation of f(123, 456) via "g".

Partial application allows specifying values for some parameters and wildcards * for others, returning a function type accepting only the wildcard parameters:

val g = f(123, *); // Type of "g" is (integer) -> integer
g(456); // Calls f(123, 456).

val h = f(*, 456); // Type of "h" is (integer) -> integer
h(123); // Calls f(123, 456).

Partial application syntax details

If a wildcard symbol * is specified for at least one parameter, all the unspecified parameters that don't have default values are also considered wildcards.

A wildcard * as the last parameter without a name has a special meaning: it marks the call as a partial application without corresponding to a specific parameter. You can write f(*) regardless of how many parameters f has (even zero).

note

If a wildcard without a name is specified as the last parameter, there must be no other wildcard parameters, as it would be redundant and confusing.

Parameters with default values that aren't explicitly specified as wildcards are bound to their default values, calculated at the moment of partial application:

function p(x: integer, y: integer = 1) = x * y;

These expressions return the same value of type (integer) -> integer:

  • p(*)
  • p(x = *)
  • p(x = *, y = 1)

To include both parameters and get (integer, integer) -> integer:

p(y = *)
p(x = *, y = *)

For a single-parameter function with a default value:

function r(x: integer = 123): integer { ... }
  • r(*) returns () -> integer (the unnamed * isn't assigned to a parameter)
  • r(x = *) returns (integer) -> integer

Named wildcard parameter ordering

The order of named wildcard parameters determines the resulting function signature:

function f(x: integer, y: text, z: boolean): decimal { ... }
  • f(*) is equivalent to f(x = *, y = *, z = *)(integer, text, boolean) -> decimal
  • f(z = *, y = *, x = *)(boolean, text, integer) -> decimal
  • f(y = *) is equivalent to f(y = *, x = *, z = *)(text, integer, boolean) -> decimal
  • f(*, z = *) is equivalent to f(x = *, z = *, y = *)(integer, boolean, text) -> decimal

Partial application of system and member functions

Most system library functions can be partially applied. For overloaded functions, you must specify the type:

val f: (integer) -> integer = abs(*); // Type annotation determines which "abs" to use

Member functions can also be partially applied:

val l = [5, 6, 7];
val f = l.size(*); // Type of "f" is () -> integer
print(f()); // Prints 3

l.add(8);
l.add(9);
print(f()); // Prints 5

Some system functions don't support partial application: print(), log(), require(), text.format(), and others.

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.
💡 Best practice
▶️ Define precise and consistent entity retrieval functions

When creating functions that retrieve entities by ID, ensure the following:

  • Use descriptive, purpose-driven names that clearly state the entity being fetched.
  • Follow the snake_case naming convention for consistency.
// Retrieve a user by account ID
function get_user(account_id: byte_array) =
require(
user @? { .account.id == account_id },
"User not found: '%s'".format(account_id)
);

// Retrieve a user account by ID
function get_user_account(id: byte_array) =
require(
user_account @? { .id == id },
"User account not found: '%s'".format(id)
);