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)
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_sizeand@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).
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.
@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
@testannotation at the module level. - Cannot take parameters: Test functions with
@testannotation must have no parameters. - Cannot use conflicting annotations/modifiers: The
@testannotation cannot be used together with:@extendableor@extendannotationsabstractoroverridemodifiers
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.
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:
- Public class: The class must be declared as
public. - Constructor: It must have a public constructor either:
- Without parameters, or
- With a single parameter of type
RellNativeEnvironment
- 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 type | Kotlin type | Java type |
|---|---|---|
big_integer | java.math.BigInteger | java.math.BigInteger |
boolean | Boolean | boolean |
byte_array | ByteArray | byte[] |
decimal | java.math.BigDecimal | java.math.BigDecimal |
gtv | net.postchain.gtv.Gtv | net.postchain.gtv.Gtv |
integer | Long | long |
rowid | Long | long |
text | String | String |
unit (return) | Unit | void |
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
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.
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)
);