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)

Size constraint annotations for parameters

Function parameters support size constraint annotations. Available annotations:

  • @size(n) / @size(a, b): Exact size or range (shorthand for @min_size and @max_size)
  • @min_size(n): Minimum size
  • @max_size(n): Maximum size

Supported types: text and byte_array. For examples and details, see Size constraint annotations for parameters.

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.

@test annotation

The @test annotation provides a new way to define test functions. The test_ naming convention still works.

Syntax

You can define tests using the test_ prefix or the @test annotation:

// Using the test_ prefix
function test_foo() {
assert_equals(2 + 2, 4);
}

// Using the @test annotation
@test function foo() {
assert_equals(2 + 2, 4);
}

Requirements and restrictions

Functions with the @test annotation must meet the following requirements:

  • Must be defined in test modules: The function must be declared within a module that has the @test annotation at the module level.
  • Cannot take parameters: Test functions with @test annotation must have no parameters.
  • Cannot use conflicting annotations/modifiers: The @test annotation cannot be used together with:
    • @extendable or @extend annotations
    • abstract or override modifiers

Compiler errors vs. silent failures

Unlike the old naming convention (test_ prefix), violations of the @test annotation requirements result in compiler errors, making issues immediately apparent. In contrast, violations with the old naming convention are silently ignored.

💡 Best practice
▶️ Use @test annotation for test functions

It is strongly recommended to use the @test annotation instead of defining tests by their name (the test_ prefix convention). The annotation provides better error detection and makes test functions explicit and clear.

Native functions

Native functions let you write function implementations in Java or Kotlin instead of Rell. This is useful for integrating external libraries or optimizing performance-critical code. Native functions are declared in Rell with the @native annotation and must be implemented in a corresponding Java or Kotlin class.

Syntax

Declare a native function using the @native annotation before the function declaration:

@native function f(a: integer, b: text): boolean;

Native functions can be called from Rell code just like regular functions. The function signature (name, parameters, and return type) must match exactly between the Rell declaration and the Java/Kotlin implementation.

Implementation requirements

For each Rell module that defines native functions, you must provide a Java or Kotlin class that implements them. The implementation class must be specified in the blockchain configuration at the path gtx.rell.native, with one class per module.

Configuration example (chromia.yml)

Add the native module mappings under config.gtx.rell.native for each blockchain that uses native functions:

blockchains:
my_chain:
module: main
config:
gtx:
rell:
native:
my_module: com.domain.pack.MyNativeModule

The keys under native are Rell module names (e.g. my_module), and the values are fully qualified Java/Kotlin class names.

Configuration example (XML)

For deployments that use raw configuration (e.g. XML/GTV format), the native mappings go under the path gtx.rell.native:

<dict>
...
<entry key="gtx">
<dict>
...
<entry key="rell">
<dict>
...
<entry key="native">
<dict>
<entry key="my_module">
<string>com.domain.pack.MyNativeModule</string>
</entry>
</dict>
</entry>
</dict>
</entry>
</dict>
</entry>
</dict>

Kotlin implementation example

class MyNativeModule(val env: RellNativeEnvironment) {
// @native function f(x: integer): integer;
fun f(x: Long): Long {
return x * x
}
}

Native module class rules

The implementation class must follow these rules:

  1. Public class: The class must be declared as public.
  2. Constructor: It must have a public constructor either:
    • Without parameters, or
    • With a single parameter of type RellNativeEnvironment
  3. Function implementation: Each native function defined in the Rell module must have a corresponding public instance (non-static) function in the class:
    • The function name must exactly match the Rell function name
    • The number of parameters must match
    • Parameter types must match (see type mapping table below)
    • Return type must match (see type mapping table below)

RellNativeEnvironment interface

The RellNativeEnvironment interface provides access to blockchain information that may be useful for native function implementations:

interface RellNativeEnvironment {
val config: Gtv
val blockchainRid: BlockchainRid
}

If your native module class constructor accepts a RellNativeEnvironment parameter, you can access this information within your native functions.

Type mappings

The following table shows how Rell types map to Kotlin and Java types in native function implementations:

Rell typeKotlin typeJava type
big_integerjava.math.BigIntegerjava.math.BigInteger
booleanBooleanboolean
byte_arrayByteArraybyte[]
decimaljava.math.BigDecimaljava.math.BigDecimal
gtvnet.postchain.gtv.Gtvnet.postchain.gtv.Gtv
integerLonglong
rowidLonglong
textStringString
unit (return)Unitvoid
T? (nullable)T?T (nullable)

Dependencies

To implement native functions, you need to add the Rell native API dependency to your project. The required Maven artifact is:

net.postchain.rell:rell-api-native:<RELL_VERSION>

Add this dependency to your project's build configuration (e.g., pom.xml for Maven or build.gradle for Gradle).

Determinism requirement

important

Native function implementations must be deterministic. This means that for the same input parameters, the function must always return the same result. Rell cannot enforce this requirement, so it is the responsibility of the native function developer to ensure determinism.

Non-deterministic functions (e.g., those that use random number generation, system time, or external APIs) can cause blockchain consensus issues and should be avoided.

💡 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)
);