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 returnsunit.
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).
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 tof(x = *, y = *, z = *)→(integer, text, boolean) -> decimalf(z = *, y = *, x = *)→(boolean, text, integer) -> decimalf(y = *)is equivalent tof(y = *, x = *, z = *)→(text, integer, boolean) -> decimalf(*, z = *)is equivalent tof(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 returnfalse. 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.
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_casenaming 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)
);