Skip to content

Functions

Functions are defined using the fn keyword:

fn greet(name: Str) Str {
"Hello, {name}!"
}

Function parameters require type annotations. Return types are specified after the parameter list. Without an explicit return type, Ard will treat the function as non-returning.

fn add(a: Int, b: Int) Int {
a + b
}
// No return type specified - this function will not return a value
fn print_message(msg: Str) {
io::print(msg)
}

In order for a function to apply side-effects or mutations to parameters, the parameter must be marked as mutable in the signature.

fn add_ten(mut value: Int) {
value += 10
}
mut count = 0
add_ten(count)
io::print(count) // 10

There is no return keyword in Ard. The last expression in a function is automatically returned:

fn multiply(x: Int, y: Int) Int {
x * y
}
fn get_status(code: Int) Str {
match code {
200 => "OK"
404 => "Not Found"
500 => "Server Error"
_ => "Unknown"
}
}

Function parameters can be marked as nullable using the ? modifier, allowing callers to omit them:

fn greet(name: Str, greeting: Str?) Str {
let msg = greeting.or("Hello")
"{msg}, {name}!"
}
// Providing a value for the nullable parameter
greet("Alice", "Hi")
// Omitting the nullable parameter (greeting becomes None)
greet("Bob")

When a non-nullable value is provided to a nullable parameter, it’s automatically wrapped in maybe::some():

fn process(data: Str, options: Options?) Result {
let opts = options.or(Options::default())
// Process with options
}
// Automatically wraps provided value as Some(Options)
process("data", Options { verbose: true })
// Omits the parameter (becomes None)
process("data")

You can omit any trailing nullable parameters in a function call. They will be treated as None:

fn configure(name: Str, timeout: Int?, retries: Int?, debug: Bool?) {
// All nullable parameters are optional
}
// You can provide all, some, or none of the nullable parameters
configure("service", 30, 3, true) // All provided
configure("service", 30, 3) // debug omitted
configure("service", 30) // retries and debug omitted
configure("service") // All nullable params omitted

Functions can be called with labelled arguments, allowing parameters to be specified in any order:

fn create_user(name: Str, age: Int, email: Str) User {
User { name: name, age: age, email: email }
}
// Positional arguments (order matters)
create_user("Alice", 25, "alice@example.com")
// Named arguments (order doesn't matter)
create_user(age: 30, email: "bob@example.com", name: "Bob")

Important: When using named arguments, all arguments must be named. Mixing positional and named arguments is not supported.

// This is NOT allowed:
create_user("Charlie", age: 35, email: "charlie@example.com")

Functions are first-class values and can be used as arguments:

fn map(list: [Int], transform: fn(Int) Int) [Int] {
let mapped: [Int] = []
for item in list {
mapped.push(transform(item))
}
mapped
}
fn double(x: Int) Int {
x * 2
}
// Pass function as argument
let numbers = [1, 2, 3, 4]
let doubled = map(numbers, double)

Functions can be defined inline without names:

let squared = map([1, 2, 3], fn(x: Int) Int { x * x })

When referring to function types, use the fn syntax and just omit the body:

let operation: fn(Int, Int) Int = add
let printer: fn(Str) = io::print
let generator: fn() Int = get_random_number