Functions and Closures
Named Functions
fn add(a: i32, b: i32) -> i32 {
return a + b
}
fn greet(name: string) {
log.info(name)
}
// Entry point; never returns
fn main() -> never {
loop { ... }
}
- All parameters require explicit types.
- Return type follows
->and may be omitted for functions that return nothing. The compiler defaults to(). returnis explicit. Flint does not use implicit last-expression returns.-> nevermarks functions that do not return (infinite loops,fatal, etc.).
Recursion
Named functions and methods may be directly or mutually recursive:
fn factorial(n: u32) -> u32 {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
On MCU targets, recursive calls are a compile error by default because stack overflow is unrecoverable. Opt in explicitly:
# flint.toml
allow-recursion = true
or
flint build --allow-recursion
On non-MCU targets, recursion is allowed unconditionally.
Callable Types and Values
Functions are first-class values:
let predicate: (i32) -> bool
let handler: (string, u32) -> Result<(), Error>
Store or pass functions by name:
fn is_even(n: i32) -> bool {
return n % 2 == 0
}
let check: (i32) -> bool = is_even
let evens = numbers.filter(is_even)
Anonymous Functions
let add_one = |x: i32| {
return x + 1
}
let evens = numbers.filter(|n| {
return n % 2 == 0
})
- Closures use
|...|, notfn(...). - Closure return types are inferred unless you write
-> T. - Closure parameter types may be omitted only when the surrounding callable type already determines them.
Anonymous functions can be passed directly to higher-order functions:
let squares = numbers.map(|v| {
return v * v
})
let total = numbers.reduce(0, |acc, v| {
return acc + v
})
Closures
Closures capture values from their enclosing scope:
let threshold = 80
let is_hot = |temp: i32| {
return temp > threshold // captures threshold
}
Closure rules:
- Closures may only be used in non-escaping contexts: local bindings and direct call arguments.
- Closures may not be returned from functions, stored in struct fields, or placed in collections.
- Copy values are copied into the closure.
- Non-copy values are captured as read-only borrowed views.
- Mutable capture of outer locals is not supported.
- Consuming a captured value from inside the closure is not supported.
You can still use mutable values inside a closure. The restriction is on capturing a mutable outer local, not on declaring or mutating locals inside the closure body:
let build_packet = |id: u8| {
let mut bytes: List<u8> = List.new()
bytes.push(0xAA)
bytes.push(id)
return bytes
}
Here bytes is a normal mutable local created inside the closure, so mutating it is fine. What Flint 1.0 does not allow is mutating an outer local by capture.
// Ok: local binding
let multiplier = 3
let triple = |x: i32| {
return x * multiplier
}
// Ok: pass as argument
let big = nums.filter(|x| { return x > threshold })
// Error: cannot store closure in struct
struct Processor {
handler: (i32) -> bool // error: closures cannot be stored in structs
}
// Error: cannot return a capturing closure
fn make_filter(n: i32) -> (i32) -> bool {
return |x| { return x > n } // error: would escape
}
For logic that needs to escape, use named functions:
fn over_threshold(x: i32) -> bool {
return x > THRESHOLD
}
fn make_filter() -> (i32) -> bool {
return over_threshold // ok: named function, no capture
}
Type Aliases for Callables
alias SensorHandler = (i32, i32) -> bool
alias Callback = () -> ()
fn register(handler: SensorHandler) { ... }
Methods
Methods are functions with a self receiver. See Structs and Enums for the full picture.