Advanced Functions and Closures
Functions are fundamental to Oxide, but there's more to them than basic definitions and calls. Let's explore advanced function patterns that enable powerful abstractions.
Function Pointers as Parameters
We can write functions that accept function pointers:
fn applyOperation(x: Int, y: Int, op: (Int, Int) -> Int): Int {
op(x, y)
}
fn add(a: Int, b: Int): Int {
a + b
}
fn multiply(a: Int, b: Int): Int {
a * b
}
fn main() {
println!("5 + 3 = \(applyOperation(5, 3, add))") // 8
println!("5 * 3 = \(applyOperation(5, 3, multiply))") // 15
}
This is straightforward, but it's less flexible than using trait bounds because function pointers can't capture variables.
Returning Function Pointers
Functions can return function pointers:
fn chooseOperation(isAddition: Bool): (Int, Int) -> Int {
if isAddition {
{ a, b -> a + b }
} else {
{ a, b -> a * b }
}
}
fn main() {
let addFn = chooseOperation(true)
let mulFn = chooseOperation(false)
println!("5 + 3 = \(addFn(5, 3))") // 8
println!("5 * 3 = \(mulFn(5, 3))") // 15
}
Returning Closures with Trait Objects
When closures capture variables, they can't be assigned to function pointer types. Instead, return a boxed trait object:
fn makeAdder(x: Int): Box<dyn Fn(Int) -> Int> {
Box.new(move { y -> x + y })
}
fn main() {
let addFive = makeAdder(5)
println!("5 + 3 = \(addFive(3))") // 8
let addTen = makeAdder(10)
println!("10 + 3 = \(addTen(3))") // 13
}
The move keyword is essential here—without it, the closure would try to borrow x, but x goes out of scope when makeAdder returns.
Returning Different Closures
If you need to return different closures from different branches, use trait objects:
fn getClosure(isDouble: Bool): Box<dyn Fn(Int) -> Int> {
if isDouble {
Box.new(move { x -> x * 2 })
} else {
Box.new(move { x -> x + 1 })
}
}
fn main() {
let f1 = getClosure(true)
let f2 = getClosure(false)
println!("\(f1(5))") // 10
println!("\(f2(5))") // 6
}
Higher-Order Functions
A function is higher-order if it takes functions as parameters or returns functions. We've already seen examples, but let's explore them more:
fn map<T, U, F>(items: Vec<T>, f: F): Vec<U>
where
F: Fn(T) -> U,
{
var result = vec![]
for item in items {
result.push(f(item))
}
result
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
let doubled = map(numbers.clone(), { x -> x * 2 })
println!("Doubled: \(doubled:?)")
let strings = map(numbers, { x -> x.toString() })
println!("Strings: \(strings:?)")
}
Flexible Function Types with Trait Bounds
Using trait bounds gives more flexibility than function pointers:
// Using function pointer - restrictive
fn processWithPointer(items: Vec<Int>, op: (Int) -> Int) {
for item in items {
println!("\(op(item))")
}
}
// Using trait bound - flexible
fn processWithTrait<F>(items: Vec<Int>, op: F)
where
F: Fn(Int),
{
for item in items {
println!("\(op(item))")
}
}
fn main() {
let numbers = vec![1, 2, 3]
// Can use closure with captured variable
let multiplier = 10
let closure = { x -> println!("\(x * multiplier)") }
// This works with trait bound
processWithTrait(numbers.clone(), closure)
// But not with function pointer
// processWithPointer(numbers, closure) // Error: closure captures multiplier
}
Closures with Trait Bounds
You can use multiple trait bounds on closure types:
fn callWithDifferentValues<F>(mut f: F)
where
F: FnMut(Int),
{
f(1)
f(2)
f(3)
}
fn main() {
var count = 0
callWithDifferentValues({ x ->
count += x
println!("Count: \(count)")
})
}
Remember the three function traits:
Fn- Immutable borrows, can call multiple timesFnMut- Mutable borrow, can call multiple timesFnOnce- Takes ownership, can call once
Function Item Types
Every function has a unique type, sometimes called a function item type:
fn add(x: Int, y: Int): Int { x + y }
fn multiply(x: Int, y: Int): Int { x * y }
fn main() {
// These have different types!
let f = add
let g = multiply
// But both can be converted to a common function pointer type
let fPtr: (Int, Int) -> Int = add
let gPtr: (Int, Int) -> Int = multiply
}
This is why you might see fn types in error messages—they're the actual types of functions, before being converted to function pointers.
Combining Trait Bounds with Closures
Complex scenarios require combining trait bounds:
fn processItems<F, G>(
items: Vec<Int>,
filter: F,
transform: G,
): Vec<Int>
where
F: Fn(Int) -> Bool,
G: Fn(Int) -> Int,
{
items
.iter()
.filter { filter(it) }
.map { transform(it) }
.collect()
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let result = processItems(
numbers,
{ x -> x % 2 == 0 }, // filter: even numbers
{ x -> x * x }, // transform: square
)
println!("Result: \(result:?)") // [4, 16, 36, 64, 100]
}
The Never Type and Functions
Functions can return the never type ! to indicate they never return:
fn failWithMessage(msg: &str)! {
panic!("\(msg)")
}
fn infiniteLoop()! {
loop {
println!("Forever!")
}
}
fn example(condition: Bool): Int {
if condition {
42
} else {
failWithMessage("Invalid condition!")
}
}
The never type allows these patterns to work: even though failWithMessage doesn't return, the if expression is valid because ! is compatible with any type.
Variadic Functions
Oxide doesn't have true variadic functions like some languages, but you can simulate them:
// Using vectors
fn sum(numbers: Vec<Int>): Int {
var total = 0
for num in numbers {
total += num
}
total
}
// Using the vec! macro
fn main() {
println!("\(sum(vec![1, 2, 3]))") // 6
println!("\(sum(vec![10, 20, 30, 40]))") // 100
}
Or with variadic arguments using macros (see the Macros chapter for more):
// Simulate variadic with macro
sum!(1, 2, 3, 4, 5)
Function Composition
Build complex operations from simpler functions:
fn compose<A, B, C, F, G>(f: F, g: G): impl Fn(A) -> C
where
F: Fn(A) -> B,
G: Fn(B) -> C,
{
move { x -> g(f(x)) }
}
fn main() {
let addOne = { x: Int -> x + 1 }
let double = { x: Int -> x * 2 }
let addOneThenDouble = compose(addOne, double)
let doubleThenAddOne = compose(double, addOne)
println!("(5 + 1) * 2 = \(addOneThenDouble(5))") // 12
println!("(5 * 2) + 1 = \(doubleThenAddOne(5))") // 11
}
Currying
Transform functions with multiple parameters into chains of single-parameter functions:
fn curry<A, B, C, F>(f: F): impl Fn(A) -> impl Fn(B) -> C
where
F: Fn(A, B) -> C + 'static,
A: 'static,
B: 'static,
C: 'static,
{
move { a -> move { b -> f(a, b) } }
}
fn add(x: Int, y: Int): Int {
x + y
}
fn main() {
let curriedAdd = curry(add)
let addFive = curriedAdd(5)
let result = addFive(3)
println!("5 + 3 = \(result)") // 8
}
Memoization Pattern
Cache function results to improve performance:
import std.collections.HashMap
struct Memoized<T> {
cache: HashMap<Int, T>,
func: (Int) -> T,
}
extension<T: Clone> Memoized<T> {
static fn new(func: (Int) -> T): Self {
Memoized {
cache: HashMap.new(),
func,
}
}
mutating fn call(x: Int): T {
if let Some(result) = cache.get(&x) {
return result.clone()
}
let result = (func)(x)
cache.insert(x, result.clone())
result
}
}
fn expensiveOperation(n: Int): Int {
println!("Computing for \(n)...")
var result = 0
for i in 0..<n {
result += i
}
result
}
fn main() {
var memo = Memoized.new(expensiveOperation)
println!("First call:")
println!("\(memo.call(5))") // Computes
println!("Second call:")
println!("\(memo.call(5))") // Returns cached value
}
Advanced Iterator Patterns
Functions and closures shine with iterators:
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// Chain operations
let result: Vec<Int> = numbers
.iter()
.filter { it % 2 == 0 }
.map { it * it }
.collect()
println!("Evens squared: \(result:?)") // [4, 16]
// Use fold to aggregate
let sum = numbers
.iter()
.fold(0) { acc, x -> acc + x }
println!("Sum: \(sum)") // 15
// Use find to search
let firstEven = numbers
.iter()
.find { it % 2 == 0 }
println!("First even: \(firstEven:?)") // Some(2)
}
Summary
Advanced functions in Oxide:
- Function pointers - Pass and return functions as values
- Trait bounds - More flexible than function pointers
- Trait objects - Return closures and different closure types
- Higher-order functions - Functions that work on other functions
- Function composition - Build complex operations from simple ones
- Currying - Transform multi-parameter functions
- Memoization - Cache function results
- Iterator patterns - Powerful function chains
These patterns form the foundation of functional programming in Oxide and enable elegant, expressive code.