Functions
Functions are the primary unit of code reuse in Vex. They are declared using the fn keyword.
Basic Syntax
fn function_name(param1: Type1, param2: Type2): ReturnType {
// bodies are blocks
return value
}Examples
// Function with no parameters and no return value
fn greet() {
$println("Hello, Vex!")
}
// Function with parameters
fn greet_user(name: string) {
$println(f"Hello, {name}!")
}
// Function with return value
fn add(a: i32, b: i32): i32 {
return a + b
}
// Single expression functions (automatic return)
fn multiply(a: i32, b: i32): i32 {
a * b
}Parameters
Immutable by Default
Parameters are immutable by default. You cannot modify them within the function body:
fn process(value: i32) {
// value = 10 // ERROR: Cannot mutate parameter
}Mutable Parameters
To make a parameter mutable, use the ! suffix:
fn increment(value!: i32) {
value = value + 1
}References
Use &T for immutable references and &T! for mutable references:
fn print_vec(data: &Vec<i32>) {
$println(f"Vector length: {data.len()}")
}
fn append_sum(data: &Vec<i32>!) {
// Note: iter() method is on &Vec<T>
let! sum = 0
for n in data {
sum += n
}
data.push(sum)
}Optional and Default Parameters
Vex supports default values for parameters:
fn greet(name: string, greeting: string = "Hello") {
$println(f"{greeting}, {name}!")
}
fn main() {
greet("Alice") // Prints: Hello, Alice!
greet("Bob", "Hi") // Prints: Hi, Bob!
}Variadic Parameters
Use ...T for functions that accept a variable number of arguments:
fn sum(numbers: ...i32): i32 {
let! total = 0
for n in numbers {
total += n
}
return total
}
let result = sum(1, 2, 3, 4, 5)Generic Functions
Functions can be generic over one or more types:
fn identity<T>(value: T): T {
return value
}
let x = identity<i32>(42)
let y = identity<string>("hello")With Contract Bounds
Constrain generic types using contracts:
fn print_it<T: $Display>(item: T) {
$println(item.toString())
}Function Overloading
Vex supports function overloading when functions share a name but have distinct parameter signatures.
fn add(a: i32, b: i32): i32 {
return a + b
}
fn add(a: f64, b: f64): f64 {
return a + b
}
let x = add(1, 2)
let y = add(1.5, 2.5)Resolution Priority
When multiple overloads are available, Vex currently prefers:
- Exact type match
- Compatible numeric coercion
- More generic fallback
- Compile error if the call is still ambiguous
Tested Scenarios
| Scenario | Status | Notes |
|---|---|---|
| Different primitive parameter types | ✅ | Covered by overload regression tests |
| Different arity | ✅ | Covered by num_args_001.vx |
| Imported overloaded functions | ✅ | Covered by import_001.vx |
| Default-parameter overloads | ✅ | Covered by default_001.vx |
| Variadic overloads | ✅ | Covered by variadic_001.vx |
| Generic fallback vs specific overload | ✅ | Covered by generic_specific_001.vx |
| Exhaustive generic + variadic + default-param combinations | ⚠️ Partial | Core cases are tested, but the full matrix is not yet exhaustive |
Current Coverage Note
Core function overloading is tested and usable today, including basic generic fallback, variadic, and default-parameter cases. However, the overload regression suite still does not exhaust every combination involving generic functions, variadic functions, and default parameters together.
Multiple Return Values (Tuples)
Vex uses tuples to return multiple values:
fn divide_with_remainder(a: i32, b: i32): (i32, i32) {
return (a / b, a % b)
}
let (quotient, remainder) = divide_with_remainder(10, 3)Methods (Go-style)
Vex uses Go-style receiver syntax for methods. Methods are defined outside the struct:
struct Point {
x: f64,
y: f64
}
// Immutable receiver
fn (self: &Point) length(): f64 {
return (self.x * self.x + self.y * self.y).sqrt()
}
// Mutable receiver
fn (self: &Point!) move_by(dx: f64, dy: f64) {
self.x += dx
self.y += dy
}
// Static/Associated function
fn Point.new(x: f64, y: f64): Point {
return Point { x, y }
}Anonymous Functions (Closures)
let add = |a: i32, b: i32| a + b
let result = add(10, 20)
// With parameter types and return type
let multiply = |a: i32, b: i32|: i32 {
return a * b
}Async Functions
Vex supports real async fn declarations and prefix await.
async fn fetch_number(): i32 {
return 42
}
async fn sum_once(): i32 {
let value = await fetch_number()
return value + 1
}
async fn main(): i32 {
let total = await sum_once()
$println(total)
return 0
}Current repo tests and examples cover:
async fnwith explicit return types- prefix
awaitinside async functions async fn main(): i32- rejection of
awaitinside ordinary sync functions
await is not a general-purpose operator for sync code. In normal code it must be used in an async context. For spawned concurrent tasks, channels, and go {} interplay, see the dedicated concurrency docs.
See also: Async/Await and Channels.
Best Practices
- Use
stringfor text - Always prefer the built-instringtype. - Prefer immutable parameters - Only use
!when necessary. - Use descriptive names - Functions should describe actions (
calculate_sum). - Keep functions focused - A function should do one thing well.
- Leverage Go-style methods - For better code organization and readability.
Next Steps
- Control Flow - Conditionals and loops
- Structs - Custom data types
- Contracts - Interface definitions