Functions
A function is a self contained part of a program. You can break up complex code into multiple programs to make it more readable, or use recursive functions to implement looping.
Function items can be declared at the top level:
fn add(a: Int, b: Int) -> Int {
a + b
}
And they can be called by other functions:
add(42, 34)
Entrypoints
The main
function is the primary entrypoint to your program. It's used to determine which symbols are used and its body is compiled verbatim (along with any functions and constants it depends on) rather than being called.
For example, the following program:
fn main() -> Int {
42
}
Will compile to the CLVM (q . 42)
.
Test Functions
Another form of entrypoint (which behaves like main
) is a test function.
This lets you write unit tests directly in your Rue code:
test fn verify() {
assert main() == 42;
}
When you run rue test
, all of the tests in the file will be executed. If they raise an error, the line number of the assertion will be included in the error message.
Exported Functions
The last kind of entrypoint is any function that is marked with export
:
export fn puzzle() -> List<Condition> {
nil
}
If you run rue build -e puzzle
, it will build this export as if it were the main function. This lets you include multiple programs inside of a single file.
Inline Functions
You can mark a function as inline
. This will change the behavior of the compiler to insert the function body everywhere it's referenced, substituting each of the parameters with the corresponding arguments in the function call, instead of defining the function separately and calling it.
As an example, this will insert the parameter value num + 10
four times, twice for each multiplication, rather than defining the function once and calling it twice:
fn main(num: Int) -> Int {
square(num + 10) + square(num + 10)
}
inline fn square(num: Int) -> Int {
num * num
}
This should be used sparingly, since it can come with either a performance hit, an increase in compiled output size, or both.
A recursive function cannot be marked as inline
.
The compiler will automatically inline functions if they are referenced a single time and their parameters are referenced a single time.
Extern Functions
Function parameters are optimized into an efficient binary tree structure, as long as the function is neither an entrypoint, nor used as a closure. This binary tree optimization is not allowed for those because it's an implementation detail of the compiler and hard to rely on as a public interface. For example, if you accept a function as an argument, its arguments must be in the expected layout.
The compiler will pick the optimized parameter layout automatically when it can, but you can opt out of this with the extern
keyword:
extern fn add(a: Int, b: Int) -> Int {
a + b
}
Just keep in mind that this can increase the size of the generated program, as well as its runtime CLVM cost.
Spread Syntax
Although it's not very useful for optimized functions (since the parameter layout is managed by the compiler anyways), you can remove the nil terminator from the parameter list in an extern function by using the spread operator. You can think of this as binding a parameter to the "rest" of the parameters in CLVM.
For example:
extern fn sum(...numbers: List<Int>) -> Int {
if numbers is nil {
0
} else {
numbers.first + sum(...numbers.rest)
}
}
However, Rue does not allow you to specify an arbitrary number of arguments, and will force you to use the spread syntax on a list manually.
So, this can be called like this:
let list = [1, 2, 3, 4, 5];
let total = sum(...list);
It's also possible to use the spread syntax with non-list types, which allows for flexibility with external functions:
fn main(
inner_puzzle: fn(...inner_solution: Any) -> List<Condition>,
...inner_solution: Any,
) -> List<Condition> {
inner_puzzle(...inner_solution)
}
Another use case for this feature is extending the list of arguments with a struct:
struct Solution {
public_key: PublicKey,
conditions: List<Condition>,
}
fn main(...solution: Solution) -> List<Condition> {
let message = tree_hash(solution.conditions);
let agg_sig = AggSigMe {
public_key: solution.public_key,
message,
};
[agg_sig, ...solution.conditions]
}
Closures
Because functions can be passed around as values, it's sometimes useful to be able to write a function inline.
You can do this with a lambda:
let adder = fn(a: Int, b: Int) => a + b;
let sum = adder(42, 34);
Creating and calling function values limits the compiler's ability to optimize the generated bytecode, since they are forced to be compatible with external programs. You should use lambdas and closures sparingly, if at all.
Captures
Lambdas can automatically capture values that are defined in their parent scope, much like how functions in general capture other functions or constants that are defined externally.
Here's an example of a lambda that captures its environment in a closure:
fn main() -> Int {
let adder = create_adder(42);
adder(100)
}
fn create_adder(a: Int) -> fn(b: Int) -> Int {
fn(b) => a + b
}
In this case, create_adder
returns a closure which captures the value of a
into the lambda function.