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:
import @std
do greet() {
std.println("Hello!")
}
do main() {
greet() // "Hello!"
}
Parameters
Parameters require type annotations:
import @std
do greet(name string) {
std.println("Hello, ${name}!")
}
do add(x int, y int) {
std.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.
import @std
// & 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() {
temp person Person = Person{name: "Alice", age: 30}
birthday(person)
std.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 |
|---|---|---|
temp | 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 = Config{debug: true}
// update_config(config) // ERROR: cannot pass immutable variable to mutable parameter
Related Errors:
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 withtemp, notconst.
Default Parameter Values
Parameters can have default values, making them optional when calling the function:
import @std
do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
do main() {
std.println(greet()) // Hello, World!
std.println(greet("Alice")) // Hello, Alice!
}
Mixed Required and Optional Parameters
Required parameters must come before parameters with defaults:
import @std
do create_player(name string, health int = 100, mana int = 50) -> string {
return "${name}: HP=${health}, MP=${mana}"
}
do main() {
std.println(create_player("Hero")) // Hero: HP=100, MP=50
std.println(create_player("Boss", 200)) // Boss: HP=200, MP=50
std.println(create_player("Wizard", 80, 150)) // Wizard: HP=80, MP=150
}
All Parameters with Defaults
import @std
do config(debug bool = false, verbose bool = true, level int = 1) -> string {
return "debug=${debug}, verbose=${verbose}, level=${level}"
}
do main() {
std.println(config()) // debug=false, verbose=true, level=1
std.println(config(true)) // debug=true, verbose=true, level=1
}
Expression Defaults
Default values can be expressions:
import @std
do calculate(multiplier float = 3.14 * 2.0) -> float {
return multiplier
}
do main() {
std.println(calculate()) // 6.28
std.println(calculate(2.0)) // 2.0
}
Grouped Parameters with Default
When grouping parameters of the same type, only the last can have a default:
import @std
do point(x, y int = 0) -> string {
return "(${x}, ${y})"
}
do main() {
std.println(point(5)) // (5, 0)
std.println(point(3, 4)) // (3, 4)
}
Default Parameter Rules
- Required parameters must come before optional parameters
- Mutable parameters (
&) cannot have default values - Default values are evaluated at call time
Related Errors:
- E2039: required parameter after parameter with default
- E2040: mutable parameter cannot have default value
Type Sharing
Parameters of the same type can share a type annotation:
import @std
// 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() {
std.println(sum(1, 2, 3)) // 6
std.println(calculate(10.0, 20.0, 3)) // 10.0
}
Return Values
Use -> to specify a return type:
import @std
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() {
temp sum int = add(10, 20)
std.println(sum) // 30
if isEven(4) {
std.println("4 is even")
}
temp name string = formatName("John", "Doe")
std.println(name) // "John Doe"
}
Multiple Return Values
Functions can return multiple values:
import @std
do divmod(dividend, divisor int) -> (int, int) {
temp quotient int = dividend / divisor
temp remainder int = dividend % divisor
return quotient, remainder
}
do minmax(a, b, c int) -> (int, int) {
temp min int = a
temp max int = 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() {
temp q, r = divmod(17, 5)
std.println("17 / 5 =", q, "remainder", r) // 3 remainder 2
temp min, max = minmax(5, 2, 8)
std.println("min:", min, "max:", max) // min: 2 max: 8
// Use _ to discard unwanted return values
temp quotient, _ = divmod(10, 3)
std.println("quotient only:", quotient) // 3
}
Array Parameters
import @std
do sum(numbers [int]) -> int {
temp total int = 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() {
temp nums [int] = {1, 2, 3, 4, 5}
std.println("Sum:", sum(nums)) // 15
temp names [string] = {"Alice", "Bob", "Charlie"}
std.println(contains(names, "Bob")) // true
std.println(contains(names, "David")) // false
}
Struct Parameters
import @std
import @math
const Point struct {
x int
y int
}
do distance(p1, p2 Point) -> float {
temp dx int = p2.x - p1.x
temp dy int = 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() {
temp a Point = Point{x: 0, y: 0}
temp b Point = Point{x: 3, y: 4}
std.println("Distance:", distance(a, b)) // 5.0
temp moved Point = translate(a, 10, 20)
std.println("Moved to:", moved.x, moved.y) // 10 20
}
Guaranteed Cleanup with ensure
The ensure keyword guarantees a function call runs when the current function exits, regardless of how it exits (normal return, early return, or reaching end of function). This prevents resource leaks by ensuring cleanup code always runs.
Problem: Resource Leaks
import @io, @db
do process_data() {
temp store, _ = db.open("mydb.ezdb")
temp file, _ = io.open("data.txt", "r")
if something_bad {
return // store and file never closed!
}
// If we forget cleanup or return early, resources leak
db.close(store)
io.close(file)
}
Solution: ensure
import @io, @db
do process_data() {
temp store, _ = db.open("mydb.ezdb")
ensure db.close(store)
temp file, _ = io.open("data.txt", "r")
ensure io.close(file)
if something_bad {
return // Both close calls still run!
}
// Even if error here, cleanup happens
}
Execution Order (LIFO)
Multiple ensure statements run in reverse order (Last-In, First-Out):
import @std
do cleanup1() { std.println("cleanup 1") }
do cleanup2() { std.println("cleanup 2") }
do cleanup3() { std.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
}
This LIFO order matches how resources are typically acquired and released - resources acquired first should be released last.
Rules
ensurestatements trigger on:- Normal return
- Early return
- Reaching end of function
- Only function calls are allowed after
ensure:ensure db.close(store) // OK 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
}
do validate(age int) -> bool {
if age < 0 {
return false // Invalid
}
if age > 150 {
return false // Invalid
}
return true
}
Void Functions
Functions without a return type don’t return a value:
import @std
do printHeader(title string) {
std.println("===================")
std.println(title)
std.println("===================")
}
do logError(message string) {
std.println("[ERROR]", message)
}
Recursion
Functions can call themselves:
import @std
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() {
std.println("5! =", factorial(5)) // 120
std.println("fib(10) =", fibonacci(10)) // 55
}
The main() Function
Every EZ program needs a main() function as its entry point:
import @std
do main() {
std.println("Program started")
// Your code here
std.println("Program finished")
}
Example Program
import @std
import @math
const Circle struct {
x float
y float
radius float
}
do createCircle(x, y, radius float) -> Circle {
return Circle{x: x, y: y, radius: radius}
}
do area(c Circle) -> float {
return math.PI * c.radius * c.radius
}
do circumference(c Circle) -> float {
return 2.0 * math.PI * c.radius
}
do scale(c Circle, factor float) -> Circle {
return Circle{
x: c.x,
y: c.y,
radius: c.radius * factor
}
}
do overlaps(c1, c2 Circle) -> bool {
temp dx float = c2.x - c1.x
temp dy float = c2.y - c1.y
temp distance float = math.sqrt(dx * dx + dy * dy)
return distance < (c1.radius + c2.radius)
}
do main() {
temp circle1 Circle = createCircle(0.0, 0.0, 5.0)
temp circle2 Circle = createCircle(8.0, 0.0, 4.0)
std.println("Circle 1:")
std.println(" Area:", area(circle1))
std.println(" Circumference:", circumference(circle1))
std.println("Circle 2:")
std.println(" Area:", area(circle2))
if overlaps(circle1, circle2) {
std.println("Circles overlap!")
} otherwise {
std.println("Circles do not overlap")
}
temp bigger Circle = scale(circle1, 2.0)
std.println("Scaled radius:", bigger.radius) // 10.0
}