Skip to content

Methods as Functions

Zig has no class syntax.

Zig has no class syntax.

A function that belongs with a type is written inside the type.

const Point = struct {
    x: i32,
    y: i32,

    pub fn moveRight(p: *Point, n: i32) void {
        p.x += n;
    }
};

The struct has two fields and one function. The function is part of the namespace of Point.

It is called with the type name:

Point.moveRight(&point, 5);

A complete program:

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn moveRight(p: *Point, n: i32) void {
        p.x += n;
    }
};

pub fn main() void {
    var point = Point{
        .x = 10,
        .y = 20,
    };

    Point.moveRight(&point, 5);

    std.debug.print("x = {d}, y = {d}\n", .{ point.x, point.y });
}

The output is:

x = 15, y = 20

There is no hidden receiver. The first parameter is ordinary. It has the type *Point, a pointer to Point.

Zig also lets this call be written with dot syntax:

point.moveRight(5);

This is the same call. Zig passes &point as the first argument because moveRight expects *Point.

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn moveRight(p: *Point, n: i32) void {
        p.x += n;
    }
};

pub fn main() void {
    var point = Point{
        .x = 10,
        .y = 20,
    };

    point.moveRight(5);

    std.debug.print("x = {d}, y = {d}\n", .{ point.x, point.y });
}

The output is still:

x = 15, y = 20

This is method-call syntax, but the method is still just a function.

A function may take the value by copy.

const Point = struct {
    x: i32,
    y: i32,

    pub fn sum(p: Point) i32 {
        return p.x + p.y;
    }
};

Call it with:

const n = point.sum();

The value point is passed as the first argument.

A complete program:

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,

    pub fn sum(p: Point) i32 {
        return p.x + p.y;
    }
};

pub fn main() void {
    const point = Point{
        .x = 7,
        .y = 8,
    };

    const n = point.sum();

    std.debug.print("{d}\n", .{n});
}

The output is:

15

Use a value parameter when the function only needs to read the value and the value is small.

Use a pointer parameter when the function must change the value.

pub fn reset(c: *Counter) void {
    c.value = 0;
}

Use a pointer to constant when the function reads a large value without copying it.

pub fn area(r: *const Rectangle) u32 {
    return r.width * r.height;
}

This says that the function receives an address, but it will not modify the rectangle through that address.

const Rectangle = struct {
    width: u32,
    height: u32,

    pub fn area(r: *const Rectangle) u32 {
        return r.width * r.height;
    }
};

Call it in the same way:

const a = rect.area();

Zig chooses the correct first argument form from the function parameter type.

If the first parameter is Rectangle, the value is passed.

If the first parameter is *Rectangle, a pointer is passed.

If the first parameter is *const Rectangle, a pointer to constant is passed.

This makes the call site short while keeping the function signature explicit.

A common style is to name the first parameter self.

const Counter = struct {
    value: u32,

    pub fn inc(self: *Counter) void {
        self.value += 1;
    }

    pub fn get(self: Counter) u32 {
        return self.value;
    }
};

self is only a parameter name. It has no special meaning in the language.

A complete counter:

const std = @import("std");

const Counter = struct {
    value: u32,

    pub fn inc(self: *Counter) void {
        self.value += 1;
    }

    pub fn add(self: *Counter, n: u32) void {
        self.value += n;
    }

    pub fn get(self: Counter) u32 {
        return self.value;
    }
};

pub fn main() void {
    var counter = Counter{
        .value = 0,
    };

    counter.inc();
    counter.add(4);

    std.debug.print("{d}\n", .{counter.get()});
}

The output is:

5

The functions are ordinary declarations. They may be called either way:

counter.inc();
Counter.inc(&counter);

Both mean the same thing.

Functions inside structs may also be constructors.

const Point = struct {
    x: i32,
    y: i32,

    pub fn init(x: i32, y: i32) Point {
        return Point{
            .x = x,
            .y = y,
        };
    }
};

Call it with the type name:

const p = Point.init(10, 20);

init is only a name. Zig does not treat it specially. It is a convention.

A type may have several constructor-like functions.

const Point = struct {
    x: i32,
    y: i32,

    pub fn init(x: i32, y: i32) Point {
        return .{
            .x = x,
            .y = y,
        };
    }

    pub fn zero() Point {
        return .{
            .x = 0,
            .y = 0,
        };
    }
};

Inside a function that returns Point, the literal may use .{ ... } because the return type is known.

return .{
    .x = 0,
    .y = 0,
};

This is the same as writing:

return Point{
    .x = 0,
    .y = 0,
};

Methods are useful because they keep operations near the data they operate on. They do not add inheritance, virtual dispatch, or hidden fields. They are namespaced functions with a convenient call form.

Exercises.

7-9. Add a method moveUp to Point that adds to y.

7-10. Define a Rectangle struct with width and height. Add an area method.

7-11. Define a Counter struct with inc, dec, and reset methods.

7-12. Write a constructor function Point.init(x, y) and use it in main.