Zig NEWS

Arya-Elfren
Arya-Elfren

Posted on • Updated on

Testing and Files as Structs

Let's build a binary tree in Zig.

Setup

  • Get Zig master
  • Create a folder for this project
  • In that folder run zig init-lib

Now our folder has a build.zig file and a src directory with main.zig in it. This is a good default structure for your code.

build.zig is written in Zig, but it constructs a declarative build graph for your project. It can be invoked with zig build. By default it also sets up tests for you which we will use later.

Struct

We want to create a data structure, that's what a struct is for! So let's create one:

const Node = struct {};
Enter fullscreen mode Exit fullscreen mode

We capitalize Node because it is a type. It is const because we have no reason to modify a type after we create it.

This will represent one node in our binary tree, so it needs to contain a piece of data and two children. We're not going to use generics yet so let us choose to store a u8.

If we made the children Nodes then this definition would be recursive and we couldn't determine its size. So instead we store pointers to other nodes. This is still not perfect as it doesn't let any Node be a leaf node. It forces every Node to have two children which would require an infinite tree, so let us make those pointers optional, resulting in ?*Node.

const Node = struct {
    data: u8,
    left: ?*Node,
    right: ?*Node,
};
Enter fullscreen mode Exit fullscreen mode

Testing

To make a unit-test we use a test block structured like so:

test "name" {
    // if any code placed here returns an error then
    // the test fails, if not it succeeds
}
Enter fullscreen mode Exit fullscreen mode

So let's make a small test that creates a Node or two.

// We use the expect function from the standard library.
// It takes a boolean and returns an error if it is false.
// Documented here: https://ziglang.org/documentation/master/std/#A;std:testing.expect
const expect = @import("std").testing.expect;

test "basic init" {
    var a = Node{
        .data = 1,
        .left = null,
        .right = null,
    };
    try expect(a.data == 1);
    try expect(a.left == null);
    try expect(a.right == null);

    // can also be created using an anonymous struct
    // if the result type is known
    const b: Node = .{
        .data = 2,
        .left = &a,
        .right = null,
    };

    try expect(b.data == 2);
    // have to unwrap the optional pointer with `.?`
    // which is equivalent to `(b.left orelse unreachable)`
    try expect(b.left.? == &a);
    try expect(a.right == null);
}
Enter fullscreen mode Exit fullscreen mode

We can run the test with zig build test, it passes.

Simplifying with defaults

Writing null every time we want to initialise our Node is unnecessary, so we default the children to initialise as null.

const Node = struct {
    data: u8,
    left: ?*Node = null,
    right: ?*Node = null,
};
Enter fullscreen mode Exit fullscreen mode

This makes our initialisation of Node a from the test above just:

var a = Node{ .data = 1 };
Enter fullscreen mode Exit fullscreen mode

Files are structs

The above code currently looks like this:

main.zig

const expect = @import("std").testing.expect;

const Node = struct {
    data: u8
    left: ?*Node = null,
    right: ?*Node = null,
};

test "basic init" {
    // snip
}
Enter fullscreen mode Exit fullscreen mode

If we wanted to separate our Node into a different file we could just copy it, but then our import would be const Node = @import("Node.zig").Node which is repetitive. Here we can use the fact that files are structs to remove a level of indentation from our definition. The above code is equivalent to this:

main.zig

const expect = @import("std").testing.expect;

const Node = @import("Node.zig");

test "basic init" {
    // snip
}
Enter fullscreen mode Exit fullscreen mode

Node.zig

const Node = @import("Node.zig");

data: u8,
left: ?*Node = null,
right: ?*Node = null,
Enter fullscreen mode Exit fullscreen mode

Here we do need to import the file from within the file, so that we can give our struct a name, Node, to use in the recursive pointers.

There is another more general way to do this which we will cover later.

Conclusion

We've created the structure of a binary tree in Zig, next we'll give it some methods and do some other things to make it more useful.

Top comments (4)

Collapse
 
deltomix profile image
DelTomix

Super helpful to see both an actual example of "zig imports files as structs" and a clear concise zig implementation of a familiar data structure.

The @import documentation mentions "Zig source files are implicitly structs" but its great to see an actual example!

Except (correct me if I'm wrong) with the bare struct contents separated into Node.zig - "Node" no longer has a declaration as the type, so for that to work wouldn't Node.zig then need to become;

data: u8,
left: ?*@This(),
right: ?*@This(),
Enter fullscreen mode Exit fullscreen mode

Or, with a 'Self' element and also using your defaults example;

const Self = @This();
data: u8,
left: ?*Self = null,
right: ?*Self = null,
Enter fullscreen mode Exit fullscreen mode

Also on that final main.zig snippet I found I needed to include the file name extension in the @import target;

const Node = @import("Node.zig");
Enter fullscreen mode Exit fullscreen mode

Wondering if maybe I should have done something in the generated build.zig ?

Collapse
 
aryaelfren profile image
Arya-Elfren

Thanks for the feedback! I fixed some of the things you pointed out.

I finally got around to compiling all the code and the final working stuff should be on the GitHub linked in the second post.

Collapse
 
kristoff profile image
Loris Cro

This is an extremely smooth introduction to creating data structures in Zig, thank you for sharing & looking forward to seeing more posts from you, if you feel like it :^)

Collapse
 
kristoff profile image
Loris Cro • Edited

Also, a small posting suggestion: giving your post a header image will make it look nicer on the main feed & on social media.