Skip to main content

Closures and Trailing Lambdas

Oxide closures use Kotlin-style brace syntax { ... } for concise anonymous functions.

Basic Syntax

Closures are written with braces, parameters before the arrow:

// No parameters
let sayHello = { println!("Hello!") }

// With parameters
let add = { a: Int, b: Int -> a + b }

// With inferred types
let double = { x -> x * 2 }

Transpiles to:

let say_hello = || println!("Hello!");
let add = |a: isize, b: isize| a + b;
let double = |x| x * 2;

Implicit it Parameter

Single-parameter closures can use the implicit it parameter:

let numbers = [1, 2, 3, 4, 5]

let doubled = numbers.iter().mapped { it * 2 }
let positives = numbers.iter().filtered { it > 0 }

Transpiles to:

let numbers = vec![1, 2, 3, 4, 5];

let doubled: Vec<_> = numbers.iter().map(|it| it * 2).collect();
let positives: Vec<_> = numbers.iter().filter(|it| *it > 0).collect();

Trailing Lambda Syntax

When a function's last parameter is a closure, it can be passed outside the parentheses:

// Standard call
numbers.forEach({ item -> println!(item) })

// Trailing lambda (preferred)
numbers.forEach { item -> println!(item) }

// With other parameters
numbers.fold(0) { acc, item -> acc + item }

// If the closure is the only argument, parentheses are optional
run { println!("Hello") }

Transpiles to:

numbers.iter().for_each(|item| println!("{}", item));
numbers.iter().fold(0, |acc, item| acc + item);
{ println!("Hello") }();

Trailing Closures in Control Flow

Important

Trailing closures are not allowed in control flow conditions. The { after a method call in if, while, for, or match is always the body, not a trailing closure.

// The { is the if body, NOT a trailing closure
if list.isEmpty() {
println!("Empty")
}

// To pass a closure in a condition, use explicit syntax:
if list.any({ it > 0 }) {
println!("Has positives")
}

// Or extract to a variable:
let hasPositives = list.any { it > 0 }
if hasPositives {
println!("Has positives")
}

Capture Closures

By default, closures capture variables by reference. Use capture to take ownership:

let name = "Alice"

// Reference capture (default)
let greet = { println!("Hello, $name") }

// Ownership capture
let greetOwned = capture { println!("Hello, $name") }

Transpiles to:

let name = "Alice";
let greet = || println!("Hello, {}", name);
let greet_owned = move || println!("Hello, {}", name);

Use capture when:

  • The closure outlives the current scope (e.g., spawning threads)
  • You want to move ownership of captured variables
fn spawnWorker(data: Vec<Int>) {
// Must capture to move data into the spawned thread
std::thread::spawn(capture {
for item in data {
process(item)
}
})
}

Multi-Line Closures

Closures can span multiple lines:

let process = { item: Item ->
let validated = validate(item)
let transformed = transform(validated)
save(transformed)
transformed
}

Async Closures

Closures can be marked as async:

// Async closure without parameters
let fetch = async { await getData() }

// Async closure with parameters
let process = async { url: String -> await fetch(url) }

// Async capture closure
let owned = async capture { await consume(data) }

Transpiles to:

let fetch = async || get_data().await;
let process = async |url: String| fetch(url).await;
let owned = async move || consume(data).await;

Common Patterns

Mapping Collections

let numbers = [1, 2, 3]
let doubled = numbers.iter().mapped { it * 2 }.toArray()
let strings = numbers.iter().mapped { "Number: $it" }.toArray()

Filtering

let evens = numbers.iter().filtered { it % 2 == 0 }.toArray()
let nonEmpty = strings.iter().filtered { !it.isEmpty() }.toArray()

Chaining Operations

let result = numbers.iter()
.filtered { it > 0 }
.mapped { it * 2 }
.filtered { it < 100 }
.toArray()

Option/Result Transformations

let value: Int? = 42
let doubled = value.mapped { it * 2 } // Some(84)
let asString = value.mapped { "Value: $it" } // Some("Value: 42")

See Also