6. Functions
Declaration#
add :: fn(a: i32, b: i32) -> i32 {
return a + b // explicit
}
double :: fn(x: i32) -> i32 {
x * 2 // implicit (last expression)
}
// A function returning nothing may omit the arrow (`-> void` is the same).
log :: fn(message: string) {
println(message)
}
// Expression-body form: `= expr` replaces a single-expression brace body.
add_one :: fn(x: i32) -> i32 = x + 1
return may appear anywhere in a body. A body that ends on an expression of the return type returns that value.
In a function with a return type, every path must produce a value of that type. Two cases are errors:
- the body reaches its closing brace without a trailing expression and without diverging:
f :: fn() -> i32 { n :: 1 }; - the trailing expression has the wrong type:
fn(c: bool) -> i32 { if c { return 7 } }ends on anifwith no value.
A body diverges when its last statement is a return, a break, a continue, a call to a @no_return function (panic, todo), or a loop that no break leaves, so fn() -> i32 { for { ... } } is valid. The same rule applies to a lambda whose result type is set by an annotation or by its slot.
A function that returns nothing omits the arrow or writes -> void (void is reserved, §3 Reserved keywords). A function type follows the same rule: fn() is fn() -> void. The last expression of such a function, or of a lambda in a slot that returns nothing, is evaluated and its value discarded: remember :: fn(seen: MutableSet<string>, s: string) { seen.add(s) } compiles although add returns a bool. A return with a value is an error there.
main entry point#
The program starts at the top-level function main. It has one of two signatures, and any other is an error:
main :: fn() -> i32 // ignore argv
main :: fn(argv: string[]) -> i32 // receive process args
argv[0]is implementation-defined: the script path undervader run, the binary path natively. The user's arguments start atargv[1].- The returned
i32is the process exit status, and0reports success. The operating system decides how much of it the parent sees: on Linux and macOS, the low 8 bits (300reads as44). - A program that stops, on a run-time check (§2 Run-time checks), a
panicor otherwise, never exits with status 0.
Default parameter values#
A parameter may declare a default value, used when a call leaves the parameter out:
greet :: fn(name: string, prefix: string = "Hello") -> string {
return "${prefix}, ${name}"
}
greet("Alice") // "Hello, Alice"
greet("Bob", "Hi") // "Hi, Bob"
greet("Eve", prefix = "Hello") // "Hello, Eve" (named parameter)
Named arguments at call site#
An argument may name its parameter, f(a = 1, b = 2), in a bare call and through UFCS alike. Named arguments may come in any order. Arguments are evaluated in the order they are written, so f(b = g(), a = h()) calls g first. These are errors:
- a positional argument after a named one;
- a name that is not a parameter of the function, or that names a parameter a positional argument already filled;
- a parameter without a default value that no argument fills.
Variadics#
Planned: a parameter written ...nums: i32[] receives the remaining arguments of a call as an array.
sum :: fn(...nums: i32[]) -> i32 {
total := 0
for n in nums { total = total + n }
return total
}
sum(1, 2, 3, 4)
Closures / lambdas#
A lambda is written params -> body, without the fn keyword.
// Single param: parens optional, expression body
items.map(x -> x * 2)
items.map((x) -> x * 2)
// Single param typed (parens required for the `:` annotation)
items.map((x: i32) -> x * 2)
// Multi-param (parens required)
arr.fold(0, (acc, x) -> acc + x)
// No-arg (empty parens required)
btn.on_click(() -> { count = count + 1 })
// Multi-statement via block-expr
items.map(x -> {
tmp :: x * x
tmp + 1
})
The -> form has no place for a return type: the type comes from the slot the lambda fills (a parameter, a field, a typed declaration) or from its body. To write the return type, use an anonymous function literal fn(params) -> R { body }, which is allowed wherever a value is expected and means the same thing, or a named function:
inc :: fn(x: i32) -> i32 { x + 1 }
items.map(inc)
A named function may be declared at the top level or inside a function body. Its parameter and return types follow §4 Type inference; a recursive function with an expression body must write its return type.
A closure captures variables by reference. An assignment to a captured variable, inside or outside the closure, is visible on both sides:
n := 0
inc :: fn() { n = n + 1 }
get :: fn() -> i32 { n }
inc()
get() // 1
Function values#
A function type fn(T1, T2, ...) -> R is an ordinary type. A function name that is not immediately called is a function value, and a lambda fills a function-typed slot directly:
add :: fn(a: i32, b: i32) -> i32 { return a + b }
main :: fn() -> i32 {
f :: add // f: fn(i32, i32) -> i32 (inferred from `add`)
return f(2, 3) // indirect call: same syntax as direct
}
cb: fn(i32) -> i32 : x -> x + 1
println(cb(41)) // 42
Handler :: struct { cb: fn(i32) -> i32 }
h :: Handler { .cb = (x) -> x * 2 }
println(h.cb(21)) // 42
UFCS (Uniform Function Call Syntax)#
plus :: fn(this: i32, other: i32) -> i32 {
return this + other
}
// Equivalent calls:
plus(2, 3)
2.plus(3)
a.f(b) is f(a, b). Vader has no methods: any free function can be called this way on its first argument.
When the struct of a declares a field f, or every variant of its union reads f the same way (§4 Common-field access), a.f reads that field even if functions named f exist. a.f(b) then calls the field if it holds a function, and otherwise the functions named f.
Outside a call, a.f is a function value: f with a bound as its first argument, taking the other parameters. a is evaluated once, when the value is taken:
show :: p.label // fn() -> string
p = q
show() // the first `p`'s label
twice(p.label) // as an argument
xs.map(p.scale) // fn(i32) -> Point, each element a `k`
plus: fn(f64) -> f64: v.add // several impls offer `add`: the slot picks one
Every kind of callee can be taken as a value: a free function, an impl member, the member of a trait value (dispatched on the value), of a union (on its tag) or of a bounded T, and an array member (xs.len, xs.push).
- A conversion of the receiver to the first parameter (an array reaching an
Iterator) runs at each call. - A member that several impls of one trait offer is picked by the function type of the slot; with no slot, it is an error.
- A generic free function is instantiated where it is taken, the receiver binding what its first parameter names and the slot the rest. A type parameter left unbound is an error.
- A static member cannot be taken as a value.
A free function is a candidate for a.f(...) when the receiver fits its first parameter in one of these ways:
- the receiver's type is assignable to the parameter's type, where a type parameter stands for any type:
m.size()on aMutableMap<string, i32>reachessize :: fn<K, V>(self: MutableMap<K, V>); - the parameter is a trait the receiver implements:
s.chars().filter(p)reachesfilter :: fn<T>(self: Iterator<T>, ...). An array fits anIterator<T>parameter here too, through its own conversion (§15 Iteration); - when no function fits by those two rules, an
Intoimpl converts the receiver to the parameter's type (§4 Type coercion viaInto): withCelsius implements Into<Label>,c.shout()callsshout :: fn(l: Label).
On a union receiver, the first parameter may be the union itself. A field only some variants have is an error (§4 Common-field access), even when a function of that name exists. The most specific candidate wins (Function overloading, below).
Import requirement#
x.foo() resolves the same way as foo(x): its candidates are the functions of the calling module, of the modules its file imports and of the prelude std/core (§11 Prelude), and no others.
// file: app/geo/geo.vader
module "app/geo"
export Square :: struct { side: f64 }
export area :: fn(s: Square) -> f64 = s.side * s.side
// file: app/paint/paint.vader
module "app/paint"
import "app/geo"
export area :: fn(s: Square) -> f64 = 4.0 * s.side
// file: app/main.vader
module "app"
import "app/geo"
import "app/paint" { area as _ } // without it, `area` is ambiguous here
square :: fn() -> Square = Square { .side = 3.0 }
total :: fn() -> f64 = square().area() // 9.0: area(square()), from app/geo
// file: app/frame.vader
module "app"
P :: import "app/paint"
frame :: fn() -> f64 = P.area(square()) // 12.0
broken :: fn() -> f64 = square().area() // error: `area` is in scope only as `P.area`
When two wildcard imports each offer a foo that fits the receiver equally well, both x.foo() and foo(x) are ambiguous.
Function overloading#
Two free functions of one module may share a name when they differ in the type of their first parameter. A call picks one by the type of its first argument, which through UFCS is the receiver.
size :: fn(self: Path) -> i32 { ... }
size :: fn<K, V>(self: MutableMap<K, V>) -> i64 { ... }
p.size() // resolves to the Path version
m.size() // resolves to the MutableMap version
These are errors:
- two functions of one module with the same name and parameter types: a redefinition, reported at the declaration even when nothing calls them. A module spans every file of its folder, so two single-file programs in one folder, each with its own
main, are a redefinition too; - a call that reaches two candidates with the same first parameter type and the same number of parameters; declaring both is not an error.
Among the candidates whose first parameter accepts the first argument, the most specific candidate wins. These ranks decide, in order:
- A parameter naming a concrete member of the argument's union beats a bare
T: for ani32 | Boomargument,T | BoombeatsT | null. A concrete overload beats a type-parameter receiver (fn<T>(self: T)) where it applies. - For an integer argument, its own type, then the narrowest integer type that holds it:
min(b, b)on au8takes thei32overload over thei64one. - On a direct call, the candidate whose number of parameters matches. Through UFCS this rank does not apply, and candidates with different numbers of parameters are never ambiguous with each other.
- For a free literal, the candidate whose parameter is the literal's default type (§4 Default integer):
abs(-5)reaches thei32overload whatever the declaration order.
If two candidates of one module are still tied after these ranks, the call is an error: x.or(-1) on x: i32 | Error | null fits or(x: T | null, d: T) and or(x: T | Error, d: T) equally. Narrow x first.
In a generic body, a call is resolved once, against the type parameters. A first argument typed by T, or by a type that mentions it (T[]), reaches only the candidates whose first parameter accepts T itself, never an overload on a concrete type, and every instance runs the candidate chosen. With describe :: fn<T: Display>(self: T) and describe :: fn(self: i32), show :: fn<T: Display>(v: T) -> string = v.describe() runs the generic one for show(1). Behaviour that must differ by type goes through a trait bound (§4 Specialization: the instance wins).
A function of the module and an imported function of the same name overload each other, and the local one wins a tie: "".is_empty() reaches the is_empty of std/string, and b.is_empty() a local is_empty :: fn(self: Box) -> bool. Two functions that two wildcard imports bring overload each other too.
Visibility#
The export keyword (§11 Visibility) is distinct from the @export decorator, which exposes a function to the host (§13 @export: exposing a function); a function may have both.