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:
- Create a function that takes three numbers and returns the largest one
- Write a function that checks if a number is even (returns
bool) - Create an error set and a function that validates input (e.g., checks if age is valid)
- Write a generic function that works with both integers and floats
- Create a function that takes a pointer and modifies the value it points to
- Write a function that returns an optional value (
?i32) - returnnullfor 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
fnand can have parameters and return values - Use
pubto make functions public, they're private by default - Error handling uses
!in return types andtry/catchwhen calling - Functions can be generic using
anytypeorcomptimetype parameters - Parameters are passed by value unless you use pointers
- The
inlinekeyword 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!