Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ().
  • return is explicit. Flint does not use implicit last-expression returns.
  • -> never marks 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 |...|, not fn(...).
  • 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.