Potato Blog


Zig Structs

Structs are a pervasive composite type and is core to zig. Importing the standard library const std = @import("std"); is essentially a struct. Let’s learn how we declare them, create instances, add functions and methods, and finally, how we leverage the @This() built-in function to give it super powers.

Declaring Structs

In zig, structs are anonymous. Let’s create a new type Vector. Here we name the struct by passing it into a variable named Vector. The name becomes the type.

const Vector = struct {
  x: i32,
  y: i32,
};

Instantiating Structs

When declaring an instance of a struct, all fields must be assigned a value. Let’s create a new Vector and print it to the terminal.

+ const std = @import("std");
+ const print = std.debug.print;

const Vector = struct {
  x: i32,
  y: i32,
};

+ pub fn main() void {
+   const v = Vector{ .x = 50, .y = 50 };
+   print("{}\n", .{v}); // main.Vector{ .x = 50, .y = 50 }
+ }

You can set default values in the struct definition. We’ll set both x and y to 0 by default.

const std = @import("std");
const print = std.debug.print;

const Vector = struct {
-  x: i32,
-  x: i32,
+  x: i32 = 0,
+  y: i32 = 0,
};

pub fn main() void {
  const v = Vector{ .x = 50, .y = 50 };
  print("{}\n", .{v});  // main.Vector{ .x = 50, .y = 50 }
}

This will change how you’re allowed to declare an instance of the struct. You only need to assign a value to a field if it’s unassigned in the definition. Assigning a value to a field that has a default value in the definition is optional. Here we skip setting a value for y, which defaults to 0.

const std = @import("std");
const print = std.debug.print;

const Vector = struct {
  x: i32 = 0,
  y: i32 = 0,
};

pub fn main() void {
-  const v = Vector{ .x = 50, .y = 50 };
+  const v = Vector{ .x = 50 };
  print("{}\n", .{v}); // main.Vector{ .x = 50, .y = 0 }
}

Struct Functions

You can declare functions inside of a struct definition. The named struct type can be used in function declarations and definitions. Let’s declare a new function named init(), which returns an anonymous struct. Zig will coerce the anonymous struct into a Vector type.

const std = @import("std");
const print = std.debug.print;

const Vector = struct {
  x: i32 = 0,
  y: i32 = 0,

+  fn init(x: i32, y: i32) Vector {
+    return .{ .x = x, .y = y }; // anonymous struct is coerced into the return type
+  }
};

pub fn main() void {
-  const v = Vector{ .x = 50 };
+  const v = Vector.init(50, 50);
  print("{}\n", .{v}); // main.Vector{ .x = 50, .y = 50 }
}

Struct Methods

You can also declare methods inside of a struct definition. The named struct type can be used in method declaration and definitions. When using struct types as a parameter to a method, by default, zig will pass the instance of the struct itself in the first parameter.

Use self in a method declaration and definition to indicate how the method is expected to be used. Naming the first parameter of a struct method self indicates to the caller that the instance of the type will be the first parameter.

Here we declare a method of Vector named sum(), which adds a Vector to a Vector instance and returns a new Vector.

const std = @import("std");
const print = std.debug.print;

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

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

+  fn sum(self: Vector, v: Vector) Vector {
+    return .{
+      .x = self.x + v.x,
+      .y = self.y + v.y,
+    };
+  }
};

pub fn main() void {
-  const v = Vector.init(50, 50);
-  print("{}\n", .{v}); // main.Vector{ .x = 50, .y = 50 }
+  const v1 = Vector{ .x = 50, .y = 50 };
+  const v2 = Vector{ .x = 20, .y = 20 };
+  const v3 = v1.sum(v2); // Vector `v1` is used as the first parameter

+  print("{}\n", .{v3}); // main.Vector{ .x = 70, .y = 70 }
}

This()

Files in zig are namedspaced struct declarations and definitions. Let’s move our Vector type to a separate file named vector.zig. @This() is a special built-in function. It returns the type of the struct that contains it. In this example @This() is namespaced to Vector and contains the file vector.zig. We must now update any function we want to call outside of this scope to public with pub.

// vector.zig
x: i32,
y: i32,

const Vector = @This();

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

pub fn sum(self: Vector, v: Vector) Vector {
  return .{
    .x = self.x + v.x,
    .y = self.y + v.y,
  }
}

Now we can import this into a project and use it.

// main.zig
const std = @import("std");
const print = std.debug.print;
const Vector = @import("vector.zig");

pub fn main() void {
  const v1 = Vector.init(25, 25);
  const v2 = Vector.init(50, 50);

  const v3 = v1.sum(v2);
  print("{}\n", .{v3}); // Vector{ .x = 75, .y = 75 }
}