4. Functions overview
This chapter defines functions and their variants: closures, methods and coroutines. This document gives the big picture before the detailed rules.
Contents
- 4.1. Function Declarations
- 4.2. Parameters
- 4.3. Return
- 4.4. Function Types
- 4.5. Closures
- 4.6. Capture Semantics
- 4.7. The
refKeyword - 4.8. Trailing Closures
- 4.9. Methods
- 4.10. Nested Functions
- 4.11. Generators (
yield) - 4.12. Async (
asyncandawait) - 4.13. Streams (
async yield) - 4.14.
throwsSignatures - 4.15.
unsafe(Function Modifier and Block Expression) - 4.16. Entry Point (
main.rok,fn main, Top-Level Statements)
The role of functions
A function is a unit of code that takes input values and produces a result value. A rok function can have all of the following characteristics.
By naming: there are named functions written fn name(...) and anonymous closures written |...|. By type annotation: a function is static or gradual. If a parameter's type is written, it is static; if omitted, generic monomorphization happens at the call site (any slot, generics). By execution model: synchronous, asynchronous and generator forms are all possible - plain functions, async fn (returns a Future), yield fn (returns an Iterator) and async yield fn (returns a Stream). By exception behavior: a function either can or cannot throw. Putting throws in the signature allows it to throw (error control flow). Finally, by ownership: a function is either a method, defined in an impl block and attached to a type, or a free function at module level.
Two forms of function definition
Functions are defined in two forms.
Named functions
The first is a function with a name.
fn area(r: f64) -> f64 {
3.14159 * r * r
}
This form consists of the fn keyword, a name, parameters, a return type and a body. It may be defined at module level or inside an impl block.
Anonymous closures
The second is a closure without a name.
|x: f64| -> f64 { 3.14159 * x * x }
|x| x * 2
|| compute()
This form encloses the parameter list in |...| followed by the body. It appears directly in expression position and may be assigned to a variable or passed directly as a function argument.
Both forms are handled by the same internal mechanism. The only differences are whether there is a name and whether an environment is captured when the closure is created.
Calls
Both functions and closures are called with ().
let a = area(2.0f64);
let f = |x: f64| x * 2;
let b = f(3.0f64);
On a call, arguments are matched to parameters. The exact matching rules are defined in Parameters.
Return
The value of the last expression of the body becomes the function's return value. Function bodies follow the block rules of block expressions.
fn add(x: i32, y: i32) -> i32 { x + y }
fn early(x: i32) -> i32 {
if x < 0 { return 0; } // explicit return for early exit
x * 2
}
The return keyword is therefore not required; the last expression is returned automatically. The detailed rules are in Return.
Functions and expressions
Functions and closures are values. They may be assigned to variables, passed as arguments to other functions, and stored in collections.
let f = |x| x * 2;
let v = list.map(f);
let g = if cond { f } else { |x| x + 1 };
Function types are defined in Function Types and consist of parameter types and a return type.
Call dispatch
Following the trait-as-type ban, function call dispatch is resolved without vtable indirection.
A named function is called directly at a statically determined function address. A closure is a pair of a captured environment and a code pointer; a call is a direct call that passes the captured environment as the first argument. A generic function is resolved to a direct call at each call site through the monomorphization of generics.
Function and closure calls are thus direct calls in principle and can be inlined. Their performance is equivalent to ordinary method calls.