Skip to content

Functions

Functions are declared with the func keyword. dusk’s function semantics come down to two rules that hold everywhere: every argument is passed by value, and every capture is an immutable copy. There are no reference types, no address-of operator, and no capture by reference.

func name(param: Type, ...) -> ReturnType {
// body
}

Parameters are written name: Type, and the return type follows ->. A minimal complete program:

add.dusk
func add(a: int64, b: int64) -> int64 {
return a + b
}
func main() -> int32 {
println(add(2, 3))
return 0
}

The main function is special-cased with a flexible signature; see Source files for its accepted forms.

All function parameters are passed by value. There are no reference types.

func foo(x: int64) -> void {
// x is a copy
}

When a pointer is passed, the pointer itself (the address value) is copied, not the heap data it points to. The callee can dereference the copy to read the heap value. The original allocation is still owned by the caller.

pointerarg.dusk
func read_plus_one(p: *int64) -> int64 {
return *p + 1 // reads the heap value through the copied pointer
}
func main() -> int32 {
x: *int64 = alloc(100)
defer free(x)
println(read_plus_one(x)) // passes a copy of the pointer, caller still owns the allocation
return 0
}

For large heap-allocated data, the caller passes a pointer to avoid copying. Ownership and the single-owner rule for pointers are covered in Memory.

Two functions cannot share a name with different signatures. The compiler rejects a second definition as a duplicate:

func f(x: int64) -> int64 { return x }
func f(x: bool) -> bool { return x }
// error: duplicate definition of 'f'

Generic functions are a different feature and are allowed. One generic function is monomorphized per use:

genid.dusk
func id<T>(x: T) -> T { return x }
func main() -> int32 {
println(id(7))
return 0
}

A lambda is an anonymous function declared with the lambda keyword. Lambdas are first-class values: they can be bound to a variable and called through it.

capture.dusk
func main() -> int32 {
factor := 3
triple := lambda (n: int64) -> int64 { return n * factor } // reads factor by copy
println(triple(5))
return 0
}

Lambdas are the argument form for the functional builtins (map, filter, reduce, fold, foreach), which are available under @paradigm functional. See Functional concepts and the paradigm system.

mapdouble.dusk
@paradigm functional
func main() -> int32 {
nums: int64[] = [1, 2, 3]
doubled := map(nums, lambda (n: int64) -> int64 { return n * 2 })
foreach(doubled, lambda (n: int64) -> void { println(n) })
return 0
}

Lambda literals are also the argument form for spawn and submit; the extra capture restrictions that apply there are described in Concurrency.

A lambda can read variables from outer scopes, captured by immutable copy. It cannot mutate them. This is the same rule that applies to nested function definitions. The copy is taken when the lambda is created. There is no capture by reference, which matches the absence of an address-of operator and pass by value everywhere.

Attempting to mutate a captured variable is a compile error, even when the variable is declared mut in the enclosing function:

func main() -> int32 {
mut total: int64 = 0
bump := lambda (n: int64) -> void { total = total + n }
// error: cannot mutate 'total' from an inner scope
return 0
}

A mut variable is only mutable within the function that declared it. Ordinary blocks in the same function, such as loop bodies and if branches, can still mutate it; only nested function definitions and closures lose mutation rights. This forces explicit data passing into inner scopes and prevents hidden state mutation through closures. See Types for the full mutability rules.

Because the copy is taken at creation, a lambda created inside a loop captures that iteration’s value, not the final one. The classic captured-loop-variable bug is inexpressible; Concurrency shows this with spawned threads.