Functions
Functions in EZ are declared with the do keyword. They support typed parameters, return values, and multiple returns.
Basic Functions
Use do to declare a function:
do greet() {
println("Hello!")
}
do main() {
greet() // "Hello!"
}
Parameters
Parameters require type annotations:
do greet(name string) {
println("Hello, ${name}!")
}
do add(x int, y int) {
println(x + y)
}
do main() {
greet("Alice") // "Hello, Alice!"
add(5, 10) // 15
}
Mutable Parameters
By default, function parameters are read-only. Use the & prefix to declare a parameter as mutable, allowing the function to modify it.
// & means "I will modify this parameter"
do birthday(&p Person) {
p.age = p.age + 1 // OK - parameter is mutable
}
// No symbol means "read-only"
do get_name(p Person) -> string {
// p.age = 100 // ERROR: cannot modify read-only parameter
return p.name
}
do main() {
mut person = Person{name: "Alice", age: 30}
birthday(person)
println(person.age) // 31
}
Rules
| Parameter Declaration | Can modify inside function? |
|---|---|
p Person | No (read-only) |
&p Person | Yes (mutable) |
| Caller Variable | To p Person | To &p Person |
|---|---|---|
mut | OK (read-only) | OK (writable) |
const | OK (read-only) | ERROR |
Passing a const variable to a mutable parameter will produce an error:
const config = Config{debug: true}
// update_config(config) // ERROR: cannot pass immutable variable to mutable parameter
Note: The
&mutable parameter syntax applies to user-defined functions only. Standard library functions that modify data (likearrays.append()) require the variable to be declared withmut, notconst.
What Can Be Passed to Mutable Parameters
Mutable parameters (&p) work with:
- Primitive variables:
increment(x) - Struct fields:
increment(point.x) - Array elements:
increment(arr[0]) - Map values:
increment(map["key"])
Default Parameter Values
Parameters can have default values, making them optional when calling the function:
do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
do main() {
println(greet()) // Hello, World!
println(greet("Alice")) // Hello, Alice!
}
Mixed Required and Optional Parameters
Required parameters must come before parameters with defaults:
do create_player(name string, health int = 100, mana int = 50) -> string {
return "${name}: HP=${health}, MP=${mana}"
}
do main() {
println(create_player("Hero")) // Hero: HP=100, MP=50
println(create_player("Boss", 200)) // Boss: HP=200, MP=50
println(create_player("Wizard", 80, 150)) // Wizard: HP=80, MP=150
}
Default Parameter Rules
- Required parameters must come before optional parameters
- Mutable parameters (
&) cannot have default values - Default values are evaluated at call time
Named Arguments
When calling a function, arguments can be passed by name using name: value syntax. This lets callers provide arguments in any order and skip over defaulted parameters to target specific ones:
do connect(host string, port int = 8080, verbose bool = false) {
if verbose {
println("Connecting to ${host}:${port}")
}
}
connect(host: "localhost", verbose: true) // port uses default 8080
connect(verbose: true, host: "localhost") // same — order doesn't matter
connect("localhost", verbose: true) // positional + named mix
Named Argument Rules
-
Positional before named: Once a named argument appears, all remaining arguments must also be named:
add(1, b: 2) // OK: positional first, then named add(a: 1, 2) // error: positional argument after named argument -
Names must match parameters exactly. Unknown names are rejected:
do add(a int, b int) -> int { return a + b } add(a: 1, c: 2) // error: unknown parameter name 'c' in call to 'add' -
No duplicate parameters. A parameter cannot be provided both positionally and by name:
add(1, a: 2) // error: parameter 'a' is already provided positionally -
Works with instance dispatch. For instance dispatch calls, the self parameter is implicit — only name the non-self parameters:
mut v = Vec{x: 2, y: 3} mut scaled = v.scale(factor: 5) // instance dispatch mut also = Vec.scale(self: v, factor: 5) // static dispatch -
Not supported for builtins or stdlib: Named arguments do not work with built-in functions (
println,len,cast, etc.) or standard library functions (strings.to_upper,math.sqrt, etc.).
Ensure Statement
The ensure statement specifies a function to call when the enclosing function exits, whether it returns normally or via early return. This is useful for cleanup tasks like closing files or releasing resources.
do cleanup() {
println("cleaning up!")
}
do process(should_bail bool) {
ensure cleanup()
println("starting work...")
if should_bail {
return // cleanup() still called
}
println("finished work!")
// cleanup() called here too
}
do main() {
process(false)
// Output:
// starting work...
// finished work!
// cleaning up!
process(true)
// Output:
// starting work...
// cleaning up!
}
Rules:
ensuretakes a function call (not a function reference)- The ensured function runs on every exit path from the enclosing function
- Multiple
ensurestatements are allowed; they run in reverse order (LIFO)
Type Sharing
Parameters of the same type can share a type annotation:
// x, y, and z all share the int type
do sum(x, y, z int) -> int {
return x + y + z
}
// a and b are floats, divisor is int
do calculate(a, b float, divisor int) -> float {
return (a + b) / float(divisor)
}
do main() {
println(sum(1, 2, 3)) // 6
println(calculate(10.0, 20.0, 3)) // 10.0
}
Return Values
Use -> to specify a return type:
do add(x, y int) -> int {
return x + y
}
do isEven(n int) -> bool {
return n % 2 == 0
}
do formatName(first, last string) -> string {
return first + " " + last
}
do main() {
mut sum = add(10, 20)
println(sum) // 30
if isEven(4) {
println("4 is even")
}
mut name = formatName("John", "Doe")
println(name) // "John Doe"
}
Multiple Return Values
Functions can return multiple values:
do divmod(dividend, divisor int) -> (int, int) {
mut quotient = dividend / divisor
mut remainder = dividend % divisor
return quotient, remainder
}
Named Return Values
You can name your return values to document what each position in the return tuple represents. Named return values are labels only — they do not declare variables in the function body. You must explicitly declare any variables you use with mut:
do divide(a, b int) -> (quotient int, remainder int) {
mut quotient int = a / b
mut remainder int = a % b
return quotient, remainder
}
mut q, r = divide(17, 5) // q=3, r=2
Named returns support grouped types (multiple names sharing one type):
do get_info() -> (name, city string, age int) {
mut name string = "Alice"
mut city string = "NYC"
mut age int = 30
return name, city, age
}
The names serve as documentation for callers and tooling (e.g., ez doc) but have no effect on the function’s scope or variable declarations.
Restriction: Wildcard types (?) cannot be used in named return positions:
// Error: wildcard type '?' cannot be named
do first(arr [?]) -> (result ?) { ... }
// OK: unnamed wildcard return
do first(arr [?]) -> ? { ... }
do divmod(dividend, divisor int) -> (int, int) {
mut quotient = dividend / divisor
mut remainder = dividend % divisor
return quotient, remainder
}
do minmax(a, b, c int) -> (int, int) {
mut min = a
mut max = a
if b < min { min = b }
if c < min { min = c }
if b > max { max = b }
if c > max { max = c }
return min, max
}
do main() {
mut q, r = divmod(17, 5)
println("17 / 5 =", q, "remainder", r) // 3 remainder 2
mut min, max = minmax(5, 2, 8)
println("min:", min, "max:", max) // min: 2 max: 8
// Use _ to discard unwanted return values
mut quotient, _ = divmod(10, 3)
println("quotient only:", quotient) // 3
}
Error Returns
Functions that may fail conventionally return a tuple with the result and an Error:
do parse(s string) -> (int, Error) {
if s == "" {
return 0, error("empty string")
}
return 42, nil
}
mut value, err = parse("test")
if err != nil {
// Handle error
}
Array Parameters
do sum(numbers [int]) -> int {
mut total = 0
for_each n in numbers {
total += n
}
return total
}
do contains(arr [string], target string) -> bool {
for_each item in arr {
if item == target {
return true
}
}
return false
}
do main() {
mut nums [int] = {1, 2, 3, 4, 5}
println("Sum:", sum(nums)) // 15
mut names [string] = {"Alice", "Bob", "Charlie"}
println(contains(names, "Bob")) // true
println(contains(names, "David")) // false
}
Struct Parameters
import @math
const Point struct {
x int
y int
}
do distance(p1, p2 Point) -> float {
mut dx = p2.x - p1.x
mut dy = p2.y - p1.y
return math.sqrt(float(dx * dx + dy * dy))
}
do translate(p Point, dx, dy int) -> Point {
return Point{x: p.x + dx, y: p.y + dy}
}
do main() {
mut a = Point{x: 0, y: 0}
mut b = Point{x: 3, y: 4}
println("Distance:", distance(a, b)) // 5.0
mut moved = translate(a, 10, 20)
println("Moved to:", moved.x, moved.y) // 10 20
}
Function References
Functions can be passed as values using the () prefix syntax or the ref() builtin:
do is_positive(n int) -> bool { return n > 0 }
// ()func_name — implicit syntax
mut check = ()is_positive
// ref(func_name) — explicit syntax
mut check2 = ref(is_positive)
// Call through the reference
check(5) // true
// Pass as argument
do filter(arr [int], test func) -> [int] {
// ...
}
mut positives = filter(numbers, ()is_positive)
Function references:
()func_nameis the implicit form (shorter)ref(func_name)is the explicit form (more readable)- Both produce identical results
- No anonymous functions or lambdas — every reference points to a named function
- The
functype is used for parameters that accept function references
Struct-Namespaced Functions
Functions can be declared inside struct blocks as namespaced free functions:
const Point struct {
x int
y int
do create(x int, y int) -> Point {
return Point{x: x, y: y}
}
do distance(a Point, b Point) -> float {
return math.sqrt(math.pow(float(a.x - b.x), 2) + math.pow(float(a.y - b.y), 2))
}
private do validate(p Point) -> bool {
return p.x >= 0 && p.y >= 0
}
}
// Called as Type.func()
mut p = Point.create(3, 4)
mut d = Point.distance(p1, p2)
Rules:
- No implicit
selforthis— every parameter is explicit privaterestricts access to other functions in the same struct- Called as
StructName.func_name(args...) - Cross-module:
module.StructName.func_name(args...) - Module-qualified types can be used in variable declarations, parameters, and return types:
mut p module.Point
Instance Dispatch
When a struct function takes the struct (or a pointer/reference to it) as its first parameter, callers can use the instance form instance.func(...) instead of writing the type name. The compiler rewrites the call as Type.func(instance, ...):
const Vec struct {
x int
y int
do len_sq(v Vec) -> int {
return v.x * v.x + v.y * v.y
}
do bump(&v Vec) {
v.x = v.x + 1
v.y = v.y + 1
}
}
mut a Vec = Vec{x: 3, y: 4}
a.len_sq() // sugar for Vec.len_sq(a)
Vec.len_sq(a) // still valid
a.bump() // sugar for Vec.bump(a); '&v' makes it a mutable alias
Both do f(v Vec) (value), do f(&v Vec) (mutable reference), and do f(v ^Vec) (pointer) participate in instance dispatch.
Factory-style functions whose first parameter isn’t the struct (e.g. do make(x int) -> Vec) require the type-namespaced form (Vec.make(...)); there is no instance to bind.
Chained calls not supported: a.f().g() is rejected. Assign each intermediate result to a variable.
Wildcard Types (?)
The ? type is a wildcard placeholder that enables generic-style functions. When used in a function’s parameter types, ? is bound to the concrete type of the argument at each call site:
do identity(x ?) -> ? {
return x
}
mut a = identity(42) // ? binds to int, returns int
mut b = identity("hello") // ? binds to string, returns string
All ? placeholders in a function signature bind to the same concrete type:
do pick_first(a ?, b ?) -> ? {
return a
}
pick_first(1, 2) // OK — both args are int
pick_first(1, "hello") // Error — conflicting bindings for ?
Wildcard types also work with composite types:
do first(arr [?]) -> ? {
return arr[0]
}
mut x = first({1, 2, 3}) // ? binds to int
mut y = first({"a", "b"}) // ? binds to string
? is only valid in function parameter types and return types. It is rejected everywhere else (variable declarations, struct fields, etc.).
Guaranteed Cleanup with ensure
The ensure keyword guarantees a function call runs when the current function exits, regardless of how it exits:
import @io
do process_data() {
mut content, _ = io.read_file("data.txt")
ensure cleanup()
if content == "" {
return // cleanup() still runs!
}
// cleanup() runs when function ends
}
Execution Order (LIFO)
Multiple ensure statements run in reverse order (Last-In, First-Out):
do cleanup1() { println("cleanup 1") }
do cleanup2() { println("cleanup 2") }
do cleanup3() { println("cleanup 3") }
do example() {
ensure cleanup1() // runs 3rd
ensure cleanup2() // runs 2nd
ensure cleanup3() // runs 1st
}
do main() {
example()
// Output:
// cleanup 3
// cleanup 2
// cleanup 1
}
Rules
ensurestatements trigger on normal return, early return, and reaching end of function- Only function calls are allowed after
ensure:ensure cleanup() // OK // ensure { block } // Not supported
Early Returns
Use return to exit a function early:
do findIndex(arr [int], target int) -> int {
for i in range(0, len(arr)) {
if arr[i] == target {
return i // Found, return early
}
}
return -1 // Not found
}
Void Functions
Functions without a return type don’t return a value:
do printHeader(title string) {
println("===================")
println(title)
println("===================")
}
do logError(message string) {
println("[ERROR]", message)
}
Visibility
By default, all functions are public. The private keyword restricts access to the declaring module:
private do validate(n int) -> bool {
return n > 0
}
do factorial(n int) -> int {
// Can call private members within the same module
if !validate(n) { return 1 }
// ...
}
Recursion
Functions can call themselves:
do factorial(n int) -> int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
do fibonacci(n int) -> int {
if n <= 1 {
return n
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
do main() {
println("5! =", factorial(5)) // 120
println("fib(10) =", fibonacci(10)) // 55
}
The main() Function
Every EZ program needs a main() function as its entry point:
do main() {
println("Program started")
// Your code here
println("Program finished")
}
See Also
- Control Flow — loops and conditionals used within functions
- Variables — variable declarations,
mutvsconst - Structs — struct types as parameters and return values
- Keywords —
do,return,ensurekeyword reference