Functions
All Helios functions are pure, which means they don't have side effects (except throwing errors) and always return the same result when given the same arguments.
Function statements
Function statements are defined using the func
keyword. Helios has no return
statement, the last expression in a function is implicitly returned:
func add(a: Int, b: Int) -> Int {
a + b
}
Anonymous functions
Helios also supports anonymous function expressions. Anonymous function expressions are basically function statements without the func
keyword:
is_even = (n: Int) -> Bool { n % 2 == 0 }
The return type of anonymous functions is optional:
is_even = (n: Int) -> { n % 2 == 0 }
Note: function statements can be referenced by their name, returning a function value. This should be preferred to using anonymous functions, as it is more readable.
First-class citizens
Functions are first-class citizens in Helios and can be used as values. This means:
Functions can be passed as arguments
even_numbers = ([]Int{1, 2, 3, 4, 5, 6}).filter(is_even) // [2, 4, 6]
Functions can be returned
add_a = (a: Int) -> (Int) -> Int { (b: Int) -> {a + b} }
Note: functions aren't entirely first-class to be precise. Functions can't be stored in containers, nor in structs/enums.
Unused arguments
All named function arguments must be used in the function definition. This can be inconvenient when defining callbacks where you want to ignore some of the arguments. For this situation you can use a standalone underscore (_
), or an underscore prefix:
// sort a map by only comparing the keys
map.sort((a_key: ByteArray, _, b_key: ByteArray, _b_value_unused) -> Bool {
a_key < b_key
})
Underscores are most commonly used to ignore either the datum or the redeemer in the main
function of a validator script.
Note: double underscore prefixes are reserved for internal use.
Optional arguments
Some function arguments can be made optional by specifying default values:
func incr(a: Int, b: Int = 1) -> Int {
a + b
}
Optional arguments must come after non-optional arguments.
The type signature of a function with optional arguments differs from a regular function:
fn: (Int, ?Int) -> Int = incr
Named arguments
Similar to literal constructor fields, function arguments can be named in a call:
func sub(a: Int, b: Int) -> Int {
a - b
}
sub(b: 1, a: 2) // == 1
A function call can't mix named arguments with positional arguments.
Named arguments are mostly used when calling the copy()
method.
Multiple return values
A Helios function can return multiple values using tuples:
func swap(a: Int, b: Int) -> (Int, Int) {
(b, a)
}
You can assign to multiple return values using tuple destructuring:
(a: Int, b: Int) = swap(10, 20) // a==20 && b==10
Some of the multiple return values can be discarded with an underscore (_
):
(a: Int, _) = swap(10, 20)
Automatic unpacking of tuples
Tuples are automatically unpacked during a function call, in a way that matches the type signature of the function being called:
(a: Int, b: Int) = swap(swap(10, 20)) // a==10 && b==20
Void return value
Functions don't have to return a value, and can return void instead, denoted by empty parentheses: ()
.
Functions that return void can't be called in assignments.
Helios has three builtin functions that return void: assert
, error
and print
.
Example
spending my_validator
import { tx } from ScriptContext
func assert_small_even(n: Int) -> () {
assert(n < 10)
assert(n % 2 == 0, "not even")
}
func main(_, _) -> () {
assert_even(tx.outputs.length)
}
assert
The builtin assert
function throws an error if a given expression evaluates to false.
The string message is mandatory, and is useful when matching failure conditions in unit-tests.
assert(condition, "should be true")
error
The builtin error
function can be used to throw errors inside branches of if-else
expressions, and cases of switch
expressions. At least one branch or case must be non-error-throwing for the if-else
or switch
expression to return a non-void value.
if (cond) {
true
} else {
error("my error message")
}
x.switch{
Buy => true,
Sell => error("my error message")
}
print
The builtin print
function is useful for debugging. print(...)
takes a String
argument:
func main() -> Bool {
print("Hello world")
true
}
Note:
Recursion
A function can call itself recursively:
func fib(n: Int) -> Int {
if (n < 1) {
1
} else {
fib(n - 1) + fib(n - 2)
}
}
Note:: a function can only reference itself when recursing. Helios doesn't support hoisting, so mutual recursion by referring to functions defined after the current function in the top-scope isn't possible (for struct and enum methods this is however possible):
01 func a(n: Int) -> Int {
02 b(n) // ReferenceError: 'b' undefined
03 }
04
05 func b(n: Int) -> Int {
06 a(n) // ok
07 }
Example: Collatz sequence
A Collatz sequence starts with a given number, n
, and calculates the next number in the sequence using the following rules:
- if
n == 1
the sequence ends - if
n
is even the next number isn / 2
- if
n
is odd the next number is(n * 3) + 1
The Collatz sequence always converges to 1
, regardless the starting number.
func collatz(n: Int, sequence: []Int) -> []Int {
updated_sequence = sequence.prepend(n)
if (n == 1) {
updated_sequence
} else if (n % 2 == 0) {
collatz(n / 2, updated_sequence)
} else {
collatz(n * 3 + 1, updated_sequence)
}
}
x = collatz(10, []Int{}) // x == []Int{10, 5, 16, 8, 4, 2, 1}