Functions

Functions are the building blocks of any program. In this lesson, we'll explore how to create and use functions in Zig, including how to handle parameters, return values, and errors.

What You'll Learn

  • How to define functions in Zig
  • Function parameters and return types
  • Public vs private functions
  • Error handling with functions
  • Function pointers and higher-order functions

Basic Function Syntax

Functions in Zig are declared using the fn keyword:

const std = @import("std");

// Basic function with no parameters or return value
fn greet() void {
    std.debug.print("Hello, Zig!\n", .{});
}

// Function with parameters
fn greetPerson(name: []const u8) void {
    std.debug.print("Hello, {s}!\n", .{name});
}

// Function with return value
fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() !void {
    greet();
    greetPerson("Alice");

    const result = add(5, 7);
    std.debug.print("5 + 7 = {}\n", .{result});
}

Return Values

Zig functions can return values explicitly or implicitly:

const std = @import("std");

// Explicit return
fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

// Implicit return (last expression is returned)
fn square(x: i32) i32 {
    x * x
}

// Multiple return statements
fn absolute(x: i32) i32 {
    if (x < 0) {
        return -x;
    }
    return x;
}

pub fn main() !void {
    std.debug.print("3 * 4 = {}\n", .{multiply(3, 4)});
    std.debug.print("5^2 = {}\n", .{square(5)});
    std.debug.print("|-10| = {}\n", .{absolute(-10)});
}

Public vs Private Functions

By default, functions are private to the file. Use pub to make them public:

const std = @import("std");

// Private function - only accessible within this file
fn privateHelper() void {
    std.debug.print("This is private\n", .{});
}

// Public function - can be accessed from other files
pub fn publicFunction() void {
    std.debug.print("This is public\n", .{});
    privateHelper(); // Private functions can call each other
}

pub fn main() !void {
    publicFunction();
    privateHelper(); // Works within the same file
}

Error Handling with Functions

Zig uses explicit error handling. Functions can return errors using the ! operator:

const std = @import("std");

// Define custom errors
const MathError = error{
    DivisionByZero,
    NegativeNumber,
};

// Function that may return an error
fn divide(a: i32, b: i32) MathError!i32 {
    if (b == 0) {
        return MathError.DivisionByZero;
    }
    return @divTrunc(a, b);
}

// Function with multiple possible errors
fn squareRoot(x: i32) MathError!i32 {
    if (x < 0) {
        return MathError.NegativeNumber;
    }
    // Simplified square root (just for demonstration)
    var i: i32 = 0;
    while (i * i <= x) : (i += 1) {}
    return i - 1;
}

pub fn main() !void {
    // Using try to propagate errors
    const result1 = try divide(10, 2);
    std.debug.print("10 / 2 = {}\n", .{result1});

    // Using catch to handle errors
    const result2 = divide(10, 0) catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    std.debug.print("Result: {}\n", .{result2});

    // Using if to handle errors conditionally
    if (squareRoot(16)) |root| {
        std.debug.print("Square root of 16: {}\n", .{root});
    } else |err| {
        std.debug.print("Error calculating square root: {}\n", .{err});
    }
}

Function Parameters

Zig supports various parameter patterns:

const std = @import("std");

// Multiple parameters
fn displayInfo(name: []const u8, age: u8, height: f32) void {
    std.debug.print("Name: {s}, Age: {}, Height: {d:.2}m\n", .{name, age, height});
}

// Mutable parameters (copy is made)
fn increment(x: i32) i32 {
    var result = x; // Create mutable copy
    result += 1;
    return result;
}

// Pass by reference using pointers
fn incrementPointer(x: *i32) void {
    x.* += 1; // Dereference and modify
}

pub fn main() !void {
    // Multiple parameters
    displayInfo("Alice", 30, 1.75);

    // Value parameters (original not modified)
    const num = 5;
    const incremented = increment(num);
    std.debug.print("Original: {}, Incremented: {}\n", .{num, incremented});

    // Pointer parameter (modifies original)
    var value: i32 = 10;
    incrementPointer(&value);
    std.debug.print("After incrementPointer: {}\n", .{value});

    // Try modifying these examples!
}

Generic Functions

Zig supports compile-time generics using anytype or explicit type parameters:

const std = @import("std");

// Generic function using anytype
fn max(a: anytype, b: anytype) @TypeOf(a, b) {
    if (a > b) {
        return a;
    }
    return b;
}

// Generic function with explicit type parameter
fn swap(comptime T: type, a: *T, b: *T) void {
    const temp = a.*;
    a.* = b.*;
    b.* = a.*;
}

pub fn main() !void {
    // Works with different types
    std.debug.print("max(5, 10) = {}\n", .{max(5, 10)});
    std.debug.print("max(3.14, 2.71) = {d:.2}\n", .{max(3.14, 2.71)});

    // Swap integers
    var x: i32 = 1;
    var y: i32 = 2;
    std.debug.print("Before swap: x={}, y={}\n", .{x, y});
    swap(i32, &x, &y);
    std.debug.print("After swap: x={}, y={}\n", .{x, y});
}

Inline Functions

You can suggest that a function be inlined for performance:

const std = @import("std");

// Suggest inlining (compiler may still decide not to)
inline fn fastMultiply(a: i32, b: i32) i32 {
    return a * b;
}

// Force inlining at every call site
fn calculate() i32 {
    return fastMultiply(3, 4) + fastMultiply(5, 6);
}

pub fn main() !void {
    std.debug.print("Result: {}\n", .{calculate()});
}

Practice Exercises

Try these exercises:

  1. Create a function that takes three numbers and returns the largest one
  2. Write a function that checks if a number is even (returns bool)
  3. Create an error set and a function that validates input (e.g., checks if age is valid)
  4. Write a generic function that works with both integers and floats
  5. Create a function that takes a pointer and modifies the value it points to
  6. Write a function that returns an optional value (?i32) - return null for invalid input

Challenge: Create a calculator function that takes two numbers and an operation (as a string like "add", "subtract") and returns the result. Handle division by zero as an error.

Key Takeaways

  • Functions are declared with fn and can have parameters and return values
  • Use pub to make functions public, they're private by default
  • Error handling uses ! in return types and try/catch when calling
  • Functions can be generic using anytype or comptime type parameters
  • Parameters are passed by value unless you use pointers
  • The inline keyword suggests function inlining for performance

Next Steps

Now that you understand functions, let's explore how to control the flow of your program using conditionals and loops.

Ready to continue? Let's learn about Control Flow!

Functions | LearningZig.org