Functions
Functions are pervasive in Oxide code. You've already seen the main function, which is the entry point of many programs. The fn keyword allows you to declare new functions.
Oxide uses camelCase for function and variable names, in contrast to Rust's snake_case. This follows the conventions of Swift, Kotlin, and TypeScript. When you call Rust code from Oxide, name conversion happens automatically.
Defining Functions
Functions are defined with fn, followed by a name, parameters in parentheses, and a body in curly braces:
fn main() {
println!("Hello, world!")
anotherFunction()
}
fn anotherFunction() {
println!("Another function.")
}
Functions can be defined before or after main; Oxide doesn't care where you define them, as long as they're in scope.
Parameters
Functions can have parameters, which are special variables that are part of the function's signature. When a function has parameters, you provide concrete values (called arguments) when you call it.
fn main() {
greet("Alice")
printSum(5, 3)
}
fn greet(name: &str) {
println!("Hello, \(name)!")
}
fn printSum(a: Int, b: Int) {
println!("\(a) + \(b) = \(a + b)")
}
Parameters must have type annotations. This is a deliberate design decision; requiring types in function signatures means the compiler rarely needs type annotations elsewhere.
Rust comparison: The syntax is identical, except Oxide uses camelCase for function names and its own type aliases.
#![allow(unused)] fn main() { // Rust fn print_sum(a: i32, b: i32) { println!("{} + {} = {}", a, b, a + b); } }
Return Values
Functions can return values. Declare the return type after a colon (:) following the parameter list:
fn five(): Int {
5
}
fn add(a: Int, b: Int): Int {
a + b
}
fn main() {
let x = five()
let sum = add(10, 20)
println!("x = \(x), sum = \(sum)")
}
The return value is the final expression in the function body. You can also return early using the return keyword:
fn absoluteValue(x: Int): Int {
if x < 0 {
return -x
}
x
}
Rust comparison: Oxide uses : for return types instead of Rust's ->. This aligns with TypeScript, Kotlin, and Swift conventions.
#![allow(unused)] fn main() { // Rust fn add(a: i32, b: i32) -> i32 { a + b } }
Statements and Expressions
Function bodies are made up of a series of statements optionally ending in an expression. Understanding the difference is important:
- Statements perform actions but don't return a value
- Expressions evaluate to a resulting value
fn main() {
// This is a statement (variable declaration)
let y = 6
// This block is an expression that evaluates to 4
let x = {
let temp = 3
temp + 1 // No semicolon - this is the block's value
}
println!("x = \(x)") // Prints: x = 4
}
Note that temp + 1 has no semicolon. Adding a semicolon would turn it into a statement, and the block would return () (the unit type) instead.
Visibility
By default, functions are private to their module. Use public to make them accessible from other modules:
public fn createUser(name: &str): User {
User { name: name.toString() }
}
fn helperFunction() {
// This is only accessible within this module
}
Rust comparison: Oxide uses public instead of pub. The word is spelled out for clarity.
#![allow(unused)] fn main() { // Rust pub fn create_user(name: &str) -> User { User { name: name.to_string() } } }
Generic Functions
Functions can be generic over types:
import std.fmt.Display
fn identity<T>(value: T): T {
value
}
fn printPair<T, U>(first: T, second: U)
where
T: Display,
U: Display,
{
println!("(\(first), \(second))")
}
fn main() {
let x = identity(42)
let s = identity("hello")
printPair(1, "one")
}
Generic constraints can be specified inline or with a where clause, just like in Rust.
Functions That Return Nothing
Functions that don't return a value implicitly return (), the unit type. You can omit the return type:
fn greet(name: &str) {
println!("Hello, \(name)!")
}
// This is equivalent:
fn greetExplicit(name: &str): () {
println!("Hello, \(name)!")
}
Functions That Never Return
Some functions never return, like those that always panic or loop forever. Use the Never type for these:
fn diverges(): Never {
panic!("This function never returns!")
}
fn infiniteLoop(): Never {
loop {
// Do something forever
}
}
Closures
Closures are anonymous functions you can store in variables or pass as arguments. Oxide uses a Swift-inspired syntax with curly braces:
fn main() {
// No parameters
let sayHello = { println!("Hello!") }
// One parameter
let double = { x -> x * 2 }
// Multiple parameters
let add = { x, y -> x + y }
// With type annotations
let parse = { s: &str -> s.parse<Int>().unwrap() }
sayHello()
println!("Double 5: \(double(5))")
println!("3 + 4: \(add(3, 4))")
}
Rust comparison: Oxide uses { params -> body } instead of Rust's |params| body.
#![allow(unused)] fn main() { // Rust let double = |x| x * 2; let add = |x, y| x + y; }
Implicit it Parameter
In trailing closures (closures passed as the last argument to a function), you can use the implicit it parameter for single-argument closures:
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// Using implicit `it`
let doubled = numbers.iter().map { it * 2 }.collect<Vec<Int>>()
// Equivalent with explicit parameter
let doubled = numbers.iter().map { x -> x * 2 }.collect<Vec<Int>>()
// Filter with `it`
let evens = numbers.iter().filter { it % 2 == 0 }
// More complex usage
let users = vec![user1, user2, user3]
let activeNames = users
.iter()
.filter { it.isActive }
.map { it.name.clone() }
.collect<Vec<String>>()
}
Important: The implicit it is only available in trailing closure position. You cannot use it in variable bindings:
// NOT allowed - it only works in trailing closures
let f = { it * 2 } // Error!
// Use explicit parameter instead
let f = { x -> x * 2 } // OK
Trailing Closure Syntax
When the last argument to a function is a closure, you can write it outside the parentheses:
fn main() {
let numbers = vec![1, 2, 3, 4, 5]
// Trailing closure syntax
numbers.forEach { println!("\(it)") }
// Equivalent to:
numbers.forEach({ println!("\(it)") })
// With other arguments
numbers.iter().fold(0, { acc, x -> acc + x })
}
Multi-Statement Closures
Closures can contain multiple statements:
fn main() {
let process = { item ->
let validated = validate(item)
let transformed = transform(validated)
transformed
}
let result = process(myItem)
}
Async Functions
Async functions allow non-blocking I/O operations. Oxide uses prefix await instead of Rust's postfix .await:
async fn fetchData(url: &str): Result<String, Error> {
let response = await client.get(url).send()?
let body = await response.text()?
Ok(body)
}
async fn main(): Result<(), Error> {
let data = await fetchData("https://example.com")?
println!("Got: \(data)")
Ok(())
}
Rust comparison: Oxide uses await expr (prefix) instead of Rust's expr.await (postfix).
#![allow(unused)] fn main() { // Rust async fn fetch_data(url: &str) -> Result<String, Error> { let response = client.get(url).send().await?; let body = response.text().await?; Ok(body) } }
The prefix await reads naturally left-to-right and matches the convention in Swift, Kotlin, JavaScript, and Python.
Function Pointers
You can store functions in variables and pass them around:
fn add(a: Int, b: Int): Int {
a + b
}
fn multiply(a: Int, b: Int): Int {
a * b
}
fn applyOperation(a: Int, b: Int, op: (Int, Int) -> Int): Int {
op(a, b)
}
fn main() {
let result1 = applyOperation(5, 3, add)
let result2 = applyOperation(5, 3, multiply)
println!("5 + 3 = \(result1)")
println!("5 * 3 = \(result2)")
}
Rust comparison: Function pointer types use : instead of -> for the return type.
#![allow(unused)] fn main() { // Rust fn apply_operation(a: i32, b: i32, op: fn(i32, i32) -> i32) -> i32 { op(a, b) } }
Summary
- Functions are declared with
fnand use camelCase names - Parameters require type annotations
- Return types follow
:(not->) - Use
public(notpub) for public visibility - Closures use
{ params -> body }syntax - The implicit
itparameter works in trailing closures - Async functions use prefix
await
Functions in Oxide are designed to be familiar to developers from modern language backgrounds while maintaining full compatibility with Rust's function system.