Types


Simple types

boolean

val using_rell = true;
if (using_rell) print("Awesome!");

integer

val user_age : integer = 26;

integer.MIN_VALUE = minimum value (-2^63)

integer.MAX_VALUE = maximum value (2^63-1)

integer(s: text, radix: integer = 10) - parse a signed string representation of an integer, fail if invalid

integer(decimal): integer - converts a decimal to an integer, rounding towards 0 (5.99 becomes 5, -5.99 becomes -5), throws an exception if the resulting value is out of range

integer.from_text(s: text, radix: integer = 10): integer - same as integer(text, integer)

integer.from_hex(text): integer - parse an unsigned HEX representation

.abs(): integer - absolute value

.max(integer): integer - maximum of two values

.max(decimal): decimal - maximum of two values (converts this integer to decimal)

.min(integer): integer - minimum of two values

.min(decimal): decimal - minimum of two values (converts this integer to decimal)

.to_text(radix: integer = 10) - convert to a signed string representation

.to_hex(): text - convert to an unsigned HEX representation

.sign(): integer - returns -1, 0 or 1 depending on the sign

decimal

Represent a real number.

val approx_pi : decimal = 3.14159;
val scientific_value : decimal = 55.77e-5;

It is not a normal floating-point type found in many other languages (like float and double in C/C++/Java):

  • decimal type is accurate when working with numbers within its range. All decimal numbers (results of decimal operations) are implicitly rounded to 20 decimal places. For instance, decimal('1E-20') returns a non-zero, while decimal('1E-21') returns a zero value.
  • Numbers are stored in a decimal form, not in a binary form, so conversions to and from a string are lossless (except when rounding occurs if there are more than 20 digits after the point).
  • Floating-point types allow to store much smaller numbers, like 1E-300; decimal can only store 1E-20, but not a smaller nonzero number.
  • Operations on decimal numbers may be considerably slower than integer operations (at least 10 times slower for same integer numbers).
  • Large decimal numbers may require a lot of space: ~0.41 bytes per decimal digit (~54KiB for 1E+131071) in memory and ~0.5 bytes per digit in a database.
  • Internally, the type java.lang.BigDecimal is used in the interpreter, and NUMERIC in SQL.

In the code one can use decimal literals:

123.456
0.123
.456
33E+10
55.77e-5

Such numbers have decimal type. Simple numbers without a decimal point and exponent, like 12345, have integer type.

decimal.PRECISION: integer = the maximum number of decimal digits in a decimal number (131072 + 20)

decimal.SCALE: integer = the maximum number of decimal digits after the decimal point (20)

decimal.INT_DIGITS: integer = the maximum number of decimal digits before the decimal point (131072)

decimal.MIN_VALUE: decimal = the smallest nonzero absolute value that can be accurately stored in a decimal (1E-20)

decimal.MAX_VALUE: decimal = the largest value that can be stored in a decimal (1E+131072 - 1)

decimal(integer): decimal - converts integer to decimal

decimal(text): decimal - converts a text representation of a number to decimal. Exponential notation is allowed. Rounds the number to 20 decimal places, if necessary. Throws an exception if the number is out of range or not a valid number.

.abs(): decimal - absolute value

.ceil(): decimal - ceiling value: rounds 1.0 to 1.0, 1.00001 to 2.0, -1.99999 to -1.0, etc.

.floor(): decimal - floor value: rounds 1.0 to 1.0, 1.9999 to 1.0, -1.0001 to -2.0, etc.

.min(decimal): decimal - minimum of two values

.max(decimal): decimal - maximum of two values

.round(scale: integer = 0): decimal - rounds to a specific number of decimal places, to a closer value. Example: round(2.49) = 2.0, round(2.50) = 3.0, round(0.12345, 3) = 0.123. Negative scales are allowed too: round(12345, -3) = 12000.

.sign(): integer - returns -1, 0 or 1 depending on the sign

.to_integer(): integer - converts a decimal to an integer, rounding towards 0 (5.99 becomes 5, -5.99 becomes -5), throws an exception if the resulting value is out of range

.to_text(scientific: boolean = false): text

text

Textual value. Same as string type in some other languages.

val placeholder = "Lorem ipsum donor sit amet";
print(placeholder.size());  // 26
print(placeholder.empty()); // false

text.from_bytes(byte_array, ignore_invalid: boolean = false) - if ignore_invalid is false, throws an exception when the byte array is not a valid UTF-8 encoded string, otherwise replaces invalid characters with a placeholder.

.empty(): boolean

.size(): integer

.compare_to(text): integer - as in Java

.starts_with(text): boolean

.ends_with(text): boolean

.contains(text): boolean - true if contains the given substring

.index_of(text, start: integer = 0): integer - returns -1 if substring is not found (as in Java)

.last_index_of(text[, start: integer]): integer - returns -1 if substring is not found (as in Java)

.sub(start: integer[, end: integer]): text - get a substring (start-inclusive, end-exclusive)

.replace(old: text, new: text)

.upper_case(): text

.lower_case(): text

.split(text): list<text> - strictly split by a separator (not a regular expression)

.trim(): text - remove leading and trailing whitespace

.matches(text): boolean - true if matches a regular expression

.to_bytes(): byte_array - convert to a UTF-8 encoded byte array

.char_at(integer): integer - get a 16-bit code of a character

.format(...) - formats a string (as in Java):

  • 'My name is <%s>'.format('Bob') - returns 'My name is <Bob>'

Special operators:

  • + : concatenation
  • [] : character access (returns single-character text)

byte_array

val user_pubkey : byte_array = x"0373599a61cc6b3bc02a78c34313e1737ae9cfd56b9bb24360b437d469efdf3b15";
print(user_pubkey.to_base64()); //A3NZmmHMazvAKnjDQxPhc3rpz9Vrm7JDYLQ31Gnv3zsV

byte_array(text) - creates a byte_array from a HEX string, e.g. '1234abcd', throws an exception if the string is not a valid HEX sequence

byte_array.from_hex(text): byte_array - same as byte_array(text)

byte_array.from_base64(text): byte_array - creates a byte_array from a Base64 string, throws an exception if the string is invalid

byte_array.from_list(list<integer>): byte_array - creates a byte_array from a list; values must be 0 - 255, otherwise an exception is thrown

.empty(): boolean

.size(): integer

.sub(start: integer[, end: integer]): byte_array - sub-array (start-inclusive, end-exclusive)

.to_hex(): text - returns a HEX representation of the byte array, e.g. '1234abcd'

.to_base64(): text - returns a Base64 representation of the byte array

.to_list(): list<integer> - list of values 0 - 255

.sha256(): byte_array - returns the sha256 digest as a byte_array

Special operators:

  • + : concatenation
  • [] : element access

rowid

Primary key of a database record, 64-bit integer, supports only comparison operations

json

Stored in Postgres as JSON type, and can be parsed to text;

val json_text = '{ "name": "Alice" }';
val json_value: json = json(json_text);
print(json_value);

json(text) - create a json value from a string; fails if not a valid JSON string

.to_text(): text - convert to string

unit

No value; cannot be used explicitly. Equivalent to unit type in Kotlin.

null

Type of null expression; cannot be used explicitly

Simple type aliases

  • pubkey = byte_array
  • name = text
  • timestamp = integer
  • tuid = text

Complex types

entity

entity user {
  key pubkey;
  index name;
}

struct

A struct is similar to an entity, but its instances exist in memory, not in a database.

struct user {
  name: text;
  address: text;
  mutable balance: integer = 0;
}

Functions available for all struct types:

T.from_bytes(byte_array): T - decode from a binary-encoded gtv (same as T.from_gtv(gtv.from_bytes(x)))

T.from_gtv(gtv): T - decode from a gtv

T.from_gtv_pretty(gtv): T - decode from a pretty-encoded gtv

.to_bytes(): byte_array - encode in binary format (same as .to_gtv().to_bytes())

.to_gtv(): gtv - convert to a gtv

.to_gtv_pretty(): gtv - convert to a pretty gtv

enum

enum account_type {
  single_key_account,
  multi_sig_account
}

entity account {
  key id: byte_array;
  mutable account_type;
  mutable args: byte_array;
}

Assuming T is an enum type:

T.values(): list<T> - returns all values of the enum, in the order of declaration

T.value(text): T - finds a value by name, throws en exception if not found

T.value(integer): T - finds a value by index, throws an exception if not found

Enum value properties:

.name: text - the name of the enum value

.value: integer - the numeric value (index) associated with the enum value

T? - nullable type

val nonexistent_user = user @? { .name == "Nonexistent Name" };
require_not_empty(nonexistent_user); // Throws exception because user doesn't exists
  • Entity attributes cannot be nullable.
  • Can be used with almost any type (except nullable, unit, null).
  • Nullable nullable (T?? is not allowed).
  • Normal operations of the underlying type cannot be applied directly.
  • Supports ?:, ?. and !! operators (like in Kotlin).

Compatibility with other types:

  • Can assign a value of type T to a variable of type T?, but not the other way round.
  • Can assign null to a variable of type T?, but not to a variable of type T.
  • Can assign a value of type (T) (tuple) to a variable of type (T?).
  • Cannot assign a value of type list<T> to a variable of type list<T?>.

Allowed operations:

  • Null comparison: x == null, x != null.
  • ?? - null check operator: x?? is equivalent to x != null
  • !! - null assertion operator: x!! returns value of x if x is not null, otherwise throws an exception
  • ?: - Elvis operator: x ?: y means x if x is not null, otherwise y
  • ?. - safe access: x?.y results in x.y if x is not null and null otherwise; similarly, x?.y() either evaluates and returns x.y() or returns null
  • require(x), require_not_empty(x): throws an exception if x is null, otherwise returns value of x

Examples:

function f(): integer? { ... }

val x: integer? = f();  // type of "x" is "integer?"
val y = x;              // type of "y" is "integer?"

val i = y!!;            // type of "i" is "integer"
val j = require(y);     // type of "j" is "integer"

val a = y ?: 456;       // type of "a" is "integer"
val b = y ?: null;      // type of "b" is "integer?"

val p = y!!;            // type of "p" is "integer"
val q = y?.to_hex();    // type of "q" is "text?"

if (x != null) {
    val u = x;          // type of "u" is "integer" - smart cast is applied to "x"
} else {
    val v = x;          // type of "v" is "integer?"
}

tuple

Examples:

  • val single_number : (integer) = (16,) - one value
  • val invalid_tuple = (789) - not a tuple (no comma)
  • val user_tuple: (integer, text) = (26, "Bob") - two values
  • val named_tuple : (x: integer, y: integer) = (32, 26) - named fields (can be accessed as named_tuple.x, named_tuple.y)
  • (integer, (text, boolean)) - nested tuple

Tuple types are compatible only if names and types of fields are the same:

  • (x:integer, y:integer) and (a:integer,b:integer) are not compatible.
  • (x:integer, y:integer) and (integer,integer) are not compatible.

Reading tuple fields:

  • t[0], t[1] - by index
  • t.a, t.b - by name (for named fields)

Unpacking tuples:

val t = (123, 'Hello');
val (n, s) = t;           // n = 123, s = 'Hello'

Works for arbitrarily nested tuples:

val (n, (p, (x, y), q)) = calculate();

Special symbol _ is used to ignore a tuple element:

val (_, s) = (123, 'Hello'); // s = 'Hello'

Variable types can be specified explicitly:

val (n: integer, s: text) = (123, 'Hello');

Unpacking can be used in a loop:

val l: list<(integer, text)> = get_tuples();
for ((x, y) in l) {
    print(x, y);
}

range

Can be used in for statement:

for(count in range(10)){
  print(count); // prints out 0 to 9
}

range(start: integer = 0, end: integer, step: integer = 1) - start-inclusive, end-exclusive (as in Python):

  • range(10) - a range from 0 (inclusive) to 10 (exclusive)
  • range(5, 10) - from 5 to 10
  • range(5, 15, 4) - from 5 to 15 with step 4, i. e. [5, 9, 13]
  • range(10, 5, -1) - produces [10, 9, 8, 7, 6]
  • range(10, 5, -3) - produces [10, 7]

Special operators:

  • in - returns true if the value is in the range (taking step into account)

gtv

A type used to repsesent encoded arguments and results of remote operation and query calls. It may be a simple value (integer, string, byte array), an array of values or a string-keyed dictionary.

Some Rell types are not Gtv-compatible. Values of such types cannot be converted to/from gtv, and the types cannot be used as types of operation/query parameters or result.

Rules of Gtv-compatibility:

  • range is not Gtv-compatible
  • a complex type is not Gtv-compatible if a type of its component is not Gtv-compatible

gtv.from_json(text): gtv - decode a gtv from a JSON string

gtv.from_json(json): gtv - decode a gtv from a json value

gtv.from_bytes(byte_array): gtv - decode a gtv from a binary-encoded form

.to_json(): json - convert to JSON

.to_bytes(): byte_array - convert to bytes

.hash(): byte_array - returns a cryptographic hash of the value

gtv-related functions:

Functions available for all Gtv-compatible types:

T.from_gtv(gtv): T - decode from a gtv

T.from_gtv_pretty(gtv): T - decode from a pretty-encoded gtv

.to_gtv(): gtv - convert to a gtv

.to_gtv_pretty(): gtv - convert to a pretty gtv

.hash(): byte_array - returns a cryptographic hash of the value (same as .to_gtv().hash())

Examples:

val g = [1, 2, 3].to_gtv();
val l = list<integer>.from_gtv(g);   // Returns [1, 2, 3]
print(g.hash());

Collection types

Collection types are:

  • list<T> - an ordered list
  • set<T> - an unordered set, contains no duplicates
  • map<K,V> - a key-value map

Collection types are mutable, elements can be added or removed dynamically.

Only a non-mutable type can be used as a map key or a set element.

Following types are mutable:

  • Collection types (list, set, map) - always.
  • Nullable type - only if the underlying type is mutable.
  • Struct type - if the struct has a mutable field, or a field of a mutable type.
  • Tuple - if a type of an element is mutable.

Creating collections:

// list
val l1 = [ 1, 2, 3, 4, 5 ];
val l2 = list<integer>();

// set
val s = set<integer>();

// map
val m1 = [ 'Bob' : 123, 'Alice' : 456 ];
val m2 = map<text, integer>();

list<T>

Ordered collection type. Accept duplication.

var messages = message @* { } ( @sort timestamp = .timestamp );
messages.add(new_message);

Constructors:

list<T>() - a new empty list

list<T>(list<T>) - a copy of the given list (list of subtype is accepted as well)

list<T>(set<T>) - a copy of the given set (set of subtype is accepted)

Methods:

.add(T): boolean - adds an element to the end, always returns true

.add(pos: integer, T): boolean - inserts an element at a position, always returns true

.add_all(list<T>): boolean

.add_all(set<T>): boolean

.add_all(pos: integer, list<T>): boolean

.add_all(pos: integer, set<T>): boolean

.clear()

.contains(T): boolean

.contains_all(list<T>): boolean

.contains_all(set<T>): boolean

.empty(): boolean

.index_of(T): integer - returns -1 if element is not found

.remove(T): boolean - removes the first occurrence of the value, return true if found

.remove_all(list<T>): boolean

.remove_all(set<T>): boolean

.remove_at(pos: integer): T - removes an element at a given position

.size(): integer

._sort() - sorts this list, returns nothing (name is _sort, because sort is a keyword in Rell)

.sorted(): list<T> - returns a sorted copy of this list

.to_text(): text - returns e. g. '[1, 2, 3, 4, 5]'

.sub(start: integer[, end: integer]): list<T> - returns a sub-list (start-inclusive, end-exclusive)

Special operators:

  • [] - element access (read/modify)
  • in - returns true if the value is in the list

set<T>

Unordered collection type. Does not accept duplication.

var my_classmates = set<user>();
my_classmates.add(alice); // return true
my_classmates.add(alice); // return false

Constructors:

set<T>() - a new empty set

set<T>(set<T>) - a copy of the given set (set of subtype is accepted as well)

set<T>(list<T>) - a copy of the given list (with duplicates removed)

Methods:

.add(T): boolean - if the element is not in the set, adds it and returns true

.add_all(list<T>): boolean - adds all elements, returns true if at least one added

.add_all(set<T>): boolean - adds all elements, returns true if at least one added

.clear()

.contains(T): boolean

.contains_all(list<T>): boolean

.contains_all(set<T>): boolean

.empty(): boolean

.remove(T): boolean - removes the element, returns true if found

.remove_all(list<T>): boolean - returns true if at least one removed

.remove_all(set<T>): boolean - returns true if at least one removed

.size(): integer

.sorted(): list<T> - returns a sorted copy of this set (as a list)

.to_text(): text - returns e. g. '[1, 2, 3, 4, 5]'

Special operators:

  • in - returns true if the value is in the set

map<K,V>

A key/value pair collection type.

var dictionary = map<text, text>();
dictionary["Mordor"] = "A place where one does not simply walk into";

Constructors:

map<K,V>() - a new empty map

map<K,V>(map<K,V>) - a copy of the given map (map of subtypes is accepted as well)

Methods:

.clear()

.contains(K): boolean

.empty(): boolean

.get(K): V - get value by key (same as [])

.put(K, V) - adds/replaces a key-value pair

.keys(): set<K> - returns a copy of keys

.put_all(map<K, V>) - adds/replaces all key-value pairs from the given map

.remove(K): V - removes a key-value pair (fails if the key is not in the map)

.size(): integer

.to_text(): text - returns e. g. '{x=123, y=456}'

.values(): list<V> - returns a copy of values

Special operators:

  • [] - get/set value by key
  • in - returns true if a key is in the map

Virtual types

A reduced data structure with Merkle tree. Type virtual<T> can be used only with following types T:

  • list<*>
  • set<*>
  • map<text, *>
  • struct
  • tuple

Additionally, types of all internal elements of T must satisfy following constraints:

  • must be Gtv-compatible
  • for a map type, the key type must be text (i. e. map<text, *>)

Operations available for all virtual types:

  • member access: [] for list and map, .name for struct and tuple
  • .to_full(): T - converts the virtual value to the original value, if the value is full (all internal elements are present), otherwise throws an exception
  • .hash(): byte_array - returns the hash of the value, which is the same as the hash of the original value.
  • virtual<T>.from_gtv(gtv): virtual<T> - decodes a virtual value from a Gtv.

Features of virtual<T>:

  • it is immutable
  • reading a member of type list<*>, map<*,*>, struct or tuple returns a value of the corresponding virtual type, not of the actual member type
  • cannot be converted to Gtv, so cannot be used as a return type of a query

Example:

struct rec { t: text; s: integer; }

operation op(recs: virtual<list<rec>>) {
    for (rec in recs) {                 // type of "rec" is "virtual<rec>", not "rec"
        val full = rec.to_full();       // type of "full" is "rec", fails if the value is not full
        print(full.t);
    }
}

virtual<list<T>>

virtual<list<T>>.from_gtv(gtv): virtual<list<T>> - decodes a Gtv

.empty(): boolean

.get(integer): virtual<T> - returns an element, same as []

.hash(): byte_array

.size(): integer

.to_full(): list<T> - converts to the original value, fails if the value is not full

.to_text(): text - returns a text representation

Special operators:

  • [] - element read, returns virtual<T> (or just T for simple types)
  • in - returns true if the given integer index is present in the virtual list

virtual<set<T>>

virtual<set<T>>.from_gtv(gtv): virtual<set<T>> - decodes a Gtv

.empty(): boolean

.hash(): byte_array

.size(): integer

.to_full(): set<T> - converts to the original value, fails if the value is not full

.to_text(): text - returns a text representation

Special operators:

  • in - returns true if the given value is present in the virtual set; the type of the operand is virtual<T>> (or just T for simple types)

virtual<map<K,V>>

virtual<map<K,V>>.from_gtv(gtv): virtual<map<K,V>> - decodes a Gtv

.contains(K): boolean - same as operator in

.empty(): boolean

.get(K): virtual<V> - same as operator []

.hash(): byte_array

.keys(): set<K> - returns a copy of keys

.size(): integer

.to_full(): map<K,V> - converts to the original value, fails if the value is not full

.to_text(): text - returns a text representation

.values(): list<virtual<V>> - returns a copy of values (if V is a simple type, returns list<V>)

Special operators:

  • [] - get value by key, fails if not found, returns virtual<V> (or just V for simple types)
  • in - returns true if a key is in the map

virtual<struct>

virtual<R>.from_gtv(gtv): R - decodes a Gtv

.hash(): byte_array

.to_full(): R - converts to the original value, fails if the value is not full


Subtypes

If type B is a subtype of type A, a value of type B can be assigned to a variable of type A (or passed as a parameter of type A).

  • T is a subtype of T?.
  • null is a subtype of T?.
  • (T,P) is a subtype of (T?,P?), (T?,P) and (T,P?).