Closures in Home
On this page 36
Closures are anonymous functions that can capture variables from their surrounding scope. Home's closure system is inspired by Rust, providing powerful capture semantics with compile-time safety.
Table of Contents
- Basic Syntax
- Capture Modes
- Closure Traits
- Type Inference
- Move Closures
- Returning Closures
- Higher-Order Functions
- Async Closures
- Best Practices
Basic Syntax
Simple Closures
// No parameters
let greet = || println("Hello!")
greet()
// Single parameter
let double = |x| x * 2
let result = double(5) // 10
// Multiple parameters
let add = |a, b| a + b
let sum = add(3, 4) // 7
// With block body
let complex = |x| {
let y = x * 2
y + 1
}
With Type Annotations
// Parameter types
let add: fn(i32, i32): i32 = |a: i32, b: i32| a + b
// Return type
let double = |x: i32|: i32 { x * 2 }
// Full annotation
let multiply = |a: f64, b: f64|: f64 {
a * b
}
Capture Modes
Home closures can capture variables in different ways:
By Reference (Immutable)
Default capture mode - borrows variables immutably.
let x = 42
let print*x = || println("x = {}", x) // Captures &x
print*x()
println("{}", x) // x is still accessible
By Mutable Reference
Captures variables mutably when needed.
let mut count = 0
let increment = || {
count += 1 // Captures &mut count
}
increment()
increment()
println("{}", count) // 2
By Move
Takes ownership of captured variables using the move keyword.
let data = vec![1, 2, 3]
let consume = move || {
println("{:?}", data) // Owns data
}
consume()
// data is no longer accessible here
By Value (Copy)
For types that implement Copy, captures create a copy.
let x = 42 // i32 implements Copy
let closure = || x + 1 // Captures copy of x
println("{}", x) // x is still accessible
Closure Traits
Home uses three closure traits (like Rust) to represent different calling conventions:
Fn - Immutable Borrow
Can be called multiple times, captures by reference.
trait Fn<Args> {
type Output
fn call(&self, args: Args): Self::Output
}
// Example
let x = 10
let add*x: impl Fn(i32): i32 = |y| x + y
println("{}", add*x(5)) // 15
println("{}", add*x(10)) // 20
FnMut - Mutable Borrow
Can be called multiple times, captures by mutable reference.
trait FnMut<Args>: Fn<Args> {
fn call*mut(&mut self, args: Args): Self::Output
}
// Example
let mut count = 0
let mut increment: impl FnMut() = || {
count += 1
}
increment()
increment()
println("{}", count) // 2
FnOnce - Consume
Can be called only once, takes ownership of captures.
trait FnOnce<Args> {
type Output
fn call*once(self, args: Args): Self::Output
}
// Example
let data = String::from("Hello")
let consume: impl FnOnce() = move || {
println("{}", data)
// data is consumed here
}
consume()
// consume() // Error: already called
Type Inference
Home infers closure types from usage:
// Type inferred from usage
let numbers = vec![1, 2, 3, 4, 5]
let doubled = numbers.map(|x| x * 2)
// Explicit types when needed
let parse: fn(&str): Result<i32, Error> = |s| {
s.parse()
}
// Generic closures
fn apply<F>(f: F, x: i32): i32
where
F: Fn(i32): i32
{
f(x)
}
let result = apply(|x| x + 1, 5) // 6
Move Closures
Use move to transfer ownership of captured variables:
fn create*closure(): impl Fn(): i32 {
let x = 42
move || x // x is moved into closure
}
let closure = create*closure()
println("{}", closure()) // 42
When to Use Move
- Returning closures - Must move captures to avoid dangling references
- Threading - Send closures to other threads
- Ownership transfer - When you want the closure to own its data
// Threading example
let data = vec![1, 2, 3]
let handle = thread::spawn(move || {
println("{:?}", data) // Owns data
})
handle.join()
Returning Closures
Closures can be returned using trait objects or impl Trait:
Using impl Trait
fn make*adder(x: i32): impl Fn(i32): i32 {
move |y| x + y
}
let add*5 = make*adder(5)
println("{}", add*5(10)) // 15
Using Box
fn make*closure(choice: bool): Box<dyn Fn(i32): i32> {
if choice {
Box::new(|x| x * 2)
} else {
Box::new(|x| x + 10)
}
}
let closure = make*closure(true)
println("{}", closure(5)) // 10
Higher-Order Functions
Closures enable functional programming patterns:
Map, Filter, Reduce
let numbers = vec![1, 2, 3, 4, 5]
// Map
let doubled = numbers.iter()
.map(|x| x * 2)
.collect()
// Filter
let evens = numbers.iter()
.filter(|x| x % 2 == 0)
.collect()
// Reduce (fold)
let sum = numbers.iter()
.fold(0, |acc, x| acc + x)
Custom Higher-Order Functions
fn apply*twice<F>(f: F, x: i32): i32
where
F: Fn(i32): i32
{
f(f(x))
}
let result = apply*twice(|x| x + 1, 5) // 7
fn compose<F, G, A, B, C>(f: F, g: G): impl Fn(A): C
where
F: Fn(B): C,
G: Fn(A): B,
{
move |x| f(g(x))
}
let add*one = |x| x + 1
let double = |x| x * 2
let add*then*double = compose(double, add*one)
println("{}", add*then*double(5)) // 12
Async Closures
Closures can be async for asynchronous operations:
// Async closure
let fetch = async || {
let response = http::get("https://api.example.com").await?
response.json().await
}
// Using async closures
async fn process*data<F, Fut>(f: F): Result<()>
where
F: Fn(): Fut,
Fut: Future<Output = Result<Data>>,
{
let data = f().await?
// Process data
Ok(())
}
process*data(async || {
fetch*from*api().await
}).await?
Best Practices
1. Prefer Borrowing Over Moving
// Good - borrows
let x = vec![1, 2, 3]
let print = || println("{:?}", x)
print()
println("{:?}", x) // x still accessible
// Only use move when necessary
let consume = move || println("{:?}", x)
2. Use Type Inference
// Good - let compiler infer
numbers.map(|x| x * 2)
// Unnecessary - explicit types
numbers.map(|x: i32|: i32 { x * 2 })
3. Keep Closures Small
// Good - focused closure
let is*even = |x| x % 2 == 0
numbers.filter(is*even)
// Avoid - too complex
numbers.filter(|x| {
let result = complex*calculation(x)
let adjusted = adjust*value(result)
validate(adjusted) && check*bounds(adjusted)
})
4. Name Closures for Clarity
// Good - named for reuse
let is*positive = |x| x > 0
let is*even = |x| x % 2 == 0
numbers.filter(is*positive).filter(is*even)
// Avoid - inline everything
numbers.filter(|x| x > 0).filter(|x| x % 2 == 0)
5. Use Move for Thread Safety
// Good - move for threads
let data = vec![1, 2, 3]
thread::spawn(move || {
process(data)
})
// Error - can't borrow across threads
thread::spawn(|| {
process(data) // Error!
})
Limitations
Current limitations of Home closures:
- No recursive closures - Closures cannot directly call themselves
- Limited type inference - Some complex cases require explicit types
- No closure in const - Closures cannot be used in const contexts
Workarounds
// Recursive function instead of recursive closure
fn factorial(n: i32): i32 {
if n <= 1 { 1 } else { n * factorial(n - 1) }
}
// Use Y-combinator for recursive closures (advanced)
let factorial = fix(|f| move |n| {
if n <= 1 { 1 } else { n * f(n - 1) }
})
Examples
Event Handlers
struct Button {
on*click: Box<dyn FnMut()>,
}
impl Button {
fn new<F>(handler: F): Button
where
F: FnMut() + 'static
{
Button {
on*click: Box::new(handler),
}
}
fn click(&mut self) {
(self.on*click)()
}
}
let mut count = 0
let mut button = Button::new(move || {
count += 1
println("Clicked {} times", count)
})
button.click() // Clicked 1 times
button.click() // Clicked 2 times
Lazy Evaluation
struct Lazy<T, F>
where
F: FnOnce(): T,
{
init: Option<F>,
value: Option<T>,
}
impl<T, F> Lazy<T, F>
where
F: FnOnce(): T,
{
fn new(init: F): Lazy<T, F> {
Lazy {
init: Some(init),
value: None,
}
}
fn get(&mut self): &T {
if self.value.is*none() {
let init = self.init.take().unwrap()
self.value = Some(init())
}
self.value.as*ref().unwrap()
}
}
let mut lazy = Lazy::new(|| {
println("Computing...")
expensive*computation()
})
// Not computed yet
println("Before")
let value = lazy.get() // Prints "Computing..."
let value2 = lazy.get() // Uses cached value
Builder Pattern with Closures
struct QueryBuilder {
filters: Vec<Box<dyn Fn(&Record): bool>>,
}
impl QueryBuilder {
fn new(): QueryBuilder {
QueryBuilder { filters: vec![] }
}
fn filter<F>(mut self, f: F): QueryBuilder
where
F: Fn(&Record): bool + 'static,
{
self.filters.push(Box::new(f))
self
}
fn execute(&self, records: &[Record]): Vec<&Record> {
records.iter()
.filter(|r| self.filters.iter().all(|f| f(r)))
.collect()
}
}
let results = QueryBuilder::new()
.filter(|r| r.age > 18)
.filter(|r| r.active)
.filter(|r| r.score > 50)
.execute(&records)