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
- Functions - Named function declarations
- Control Flow - if, match, loops
- Standard Library - Iterator extensions