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.

use go:fmt
fn add(a: Int, b: Int) Int {
a + b
}
// No return type specified - this function will not return a value.
// Equivalent to declaring `Void` as the return type
fn print_message(msg: Str) {
fmt::Println(msg)
}

A mut T parameter receives an actual mutable-reference value. Callers must pass an existing reference or create one explicitly with mut expression; declaring an ordinary binding with mut is not enough.

struct Person { name: Str, age: Int }
fn grow_older(person: mut Person) {
person.age =+ 1
}
let alice = Person{name: "Alice", age: 30}
grow_older(mut alice)
let alice_reference = mut alice
grow_older(alice_reference)

Reference parameters may mutate fields and call mutating methods, but Ard source does not support replacing a whole referent through the parameter. A function that needs an ordinary value must request T; callers with mut T use postfix .@ explicitly.

fn snapshot(person: mut Person) Person {
person.@
}

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::new():

struct Options {
verbose: Bool,
}
fn process(data: Str, options: Options?) {
let opts = options.or(Options{verbose: false})
// Process with options
}
// Automatically wraps the provided value in Maybe
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:

struct User {
name: Str,
age: Int,
email: Str,
}
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")

Positional and named arguments can be mixed, but positional arguments must come first:

// Allowed: positional, then named
create_user("Charlie", age: 35, email: "charlie@example.com")
// NOT allowed: positional after named
create_user(name: "Charlie", 35, "charlie@example.com")

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

fn apply(value: Int, transform: fn(Int) Int) Int {
transform(value)
}
fn double(x: Int) Int {
x * 2
}
// Pass a named function as an argument
let doubled = apply(4, double)

Functions can be defined inline without names:

fn apply(value: Int, transform: fn(Int) Int) Int {
transform(value)
}
let squared = apply(3, fn(x: Int) Int { x * x })

Anonymous functions may also use enclosing function or receiver generics as explicit call type arguments. See Generics in anonymous functions.

A named function declared inside another function is a local closure. It captures values when its declaration executes, and direct calls and function references use that same captured closure:

fn make_offsetter(offset: Int) fn(Int) Int {
fn add(value: Int) Int {
offset + value
}
add
}
let add_two = make_offsetter(2)
let result = add_two(40) // 42

Ordinary captured values and existing reference handles are snapshotted when the declaration executes. If the local function assigns to an enclosing mutable binding, it captures that binding’s stable slot instead, so the update remains visible outside the function.

Local declarations are visible from their declaration through the remainder of the block. Their own name is available in their body, so direct recursion works, but calls before the declaration and mutual recursion through a later declaration are not supported. When a nested named declaration is the block’s final expression, it evaluates to the bound closure just as writing its name after the declaration would.

Local named function signatures cannot contain generic parameters. Move a generic named helper to module scope, or use an anonymous function value when its signature refers to an enclosing generic. Type-qualified static function declarations, such as fn User::new() User, must also be declared at module scope.

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

use go:fmt
fn add(a: Int, b: Int) Int { a + b }
fn shout(msg: Str) { fmt::Println(msg) }
fn get_random_number() Int { 4 }
let operation: fn(Int, Int) Int = add
let printer: fn(Str) = shout
let generator: fn() Int = get_random_number

A captured variadic Go callable uses ...T for its final element type, such as fn(Str, ...Str) Str. This type-only syntax does not declare an Ard variadic function. Calls may forward a list or list reference as one final spread argument; see Go interop.

Use ? after the function type for nullable function values. If the function type has an explicit return type, wrap the whole type in parentheses so the ? applies to the function instead of the return type:

fn lookup_name(id: Int) Str? {
Maybe::new<Str>()
}
let optional_printer: fn(Str)? = Maybe::new() // nullable fn(Str) Void
let optional_mapper: (fn(Int) Str)? = Maybe::new() // nullable fn(Int) Str
let maybe_name: fn(Int) Str? = lookup_name // non-null function returning Str?

fn(Int) Void? is rejected because it is ambiguous and usually means an optional callback. Use fn(Int)? or (fn(Int) Void)? instead.