Wednesday | 28 FEB 2024
[ previous ]
[ next ]

Learning Zig

Title:
Date: 2023-10-04
Tags:  zig

I want to learn zig so that I can extend ScarletDME with zig. I think this would be a fun way to learn a modern language while also using it in something I find personally interesting.

I have already got zig building scarletdme and I have a very small test zig function that is callable from scarletdme. This means that using zig is very much valid and so now I can focus on learning zig before diving back into scarletdme.

Extending a C Project with Zig

I tried ziglings and the learnzig material but I found that I didn't like it. Ziglings was fun the first time I did it but I couldn't get used to it this time around. I wanted to really just write a program, not fix things. Learnzig had a similar issue. I can see that it is a helpful guide but it felt like the description of the language rather than any real code being written.

Ziglings

ZigLearn

I found an article with some code for a guessing game that I took and then extended. This was a simple thing but I think I learned a bit more about zig now and I have an idea of the strange parts of zig.

Learn Zig Programming

I think a helpful thing might be to go line by line of the code and figure out exactly what is going on. I am also going to link to the zig documentation for each keyword and summarize it here.

Dissecting a Zig Program

We will be going through the documentation:

Zig Documentation

First the code!

const std = @import("std");

fn get_input(guesses: i32) !i64 {
    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut().writer();
    
    var buf: [10]u8 = undefined;
    
    try stdout.print("({d}) Guess a number between 1 and 100: ", .{guesses});
    
    if (try stdin.readUntilDelimiterOrEof(buf[0..],'\n')) |user_input| {
        if (std.mem.eql(u8, user_input,"x")) {
            return error.Exit;
        } else {
            const x = std.fmt.parseInt(i64, user_input, 10);
            return x;
        }
    } else {
        return error.InvalidParam;
    }
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    
    var prng = std.rand.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.os.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });
    
    const value = prng.random().intRangeAtMost(i64, 1, 100);
    
    var winCon = false;
    var guesses: i32 = 0;
    
    while (true) {
        guesses = guesses + 1;
        
        const guess = get_input(guesses) catch |err| {
            switch (err) {
                error.InvalidCharacter => {
                    try stdout.print("\x1b[31mPlease enter a number.\x1b[0m\n",.{});
                    continue;
                },
                error.Exit => break,
                else => return err
            }
        };
        
        if (guess == value) {
            winCon = true;
            break;
        }
        
        const message = if (guess < value) "\x1b[33mlow\x1b[0m" else "\x1b[31mhigh\x1b[0m";
        try stdout.print("{d} is too {s}.\n",.{guess, message});
    }
    
    if (winCon) {
        try stdout.print("\x1b[32m({d}) Right! The number was {d}.\x1b[0m",.{guesses, value});
    } else {
        try stdout.print("Bye!",.{});
    }
}

This is a simple program but it goes over quite a bit of the zig language and exposes various idioms. I'm sure it's missing quite a bit but I think going through it and the documentation is going to be quite fun.

Let's get started!

const

We immediately hit the first keyword, const. I can already guess what it means but we can read more about it here:

const

Const lets you assign something to an identifier. Var is used when you want to be able to modify a variable.

@import

Import looks very straightforward as well and indeed it is. The documentation explains that import will bring in a zig file if it already hasn't been imported. This means that zig handles making sure an import is only imported once.

It also explains that only functions that have the pub keyword can be accessed through the import. The import statement brings in the file as a struct. The import statement can take paths or the name of a package which is quite handy.

@import

std in the file above could be thought of as a zig file with the various fields like io and rand as being functions that are exposed via pub.

std

The Zig Standard Library can be found at:

std

fn

Functions are quite interesting in zig, the parameters can be pass by value or by reference and it ultimately is up to the zig compiler to decide how it wants to do it. This is because parameters are immutable which means that they cannot change. This reads like it will be a big deal in the future.

fn

i32, i64

There is a list of various primitives that we can take a look at:

Primitive Types

!

The bang symbol, the exclaimation mark, the ! is the error union operator when used in the return type. The !u64 means that the function above can return a u64 or it can return an error. This union is what the bang symbol is denoting.

Error Union Type

var

Var, as mentioned above, means that we are creating an identifier that we can later modify.

var

An interesting thing to note is that you cannot shadow variables from an outer scope in zig.

Variables

undefined

Undefined means that the variable has not been initialized.

undefined

try

Try is definitely one of the things specific to zig and it looks like a handy way to deal with errors. I really like the idea of prepending instead of adding something at the end like in rust. It also reads very much like English which I do find useful.

Try will basically execute something, if it errors it will immediately return the error.

try

if

The regular old if statement. The neat thing with if is that you can execute a try inside the if statement and this will let you handle the error case. This is what happens in the get_input function above.

The if statement also let's you bind the result of the try to user-input if it succeeds. I think rewriting this to being user_input = try ... would be the same thing. This if syntax however seems to be idiomatic zig code. I'm not sure why yet but it does look good.

The more simple thing to note is that the if statement is:

if () {
} else if () {
} else {
}

error

You can create an error on the file as you can see in the above code, I created a specific error when the user presses X. This was so I didn't have to pass an exit flag up, instead I could return an error and then catch it on the other side.

This method seems hacky but I like how it works. The docs mention that currently you can only have u16 number of errors in the entire zig compilation. That is 65535 errors you can define.

error

return

I couldn't find the section on return as it seems to be everywhere. At least it is obvious what the statement is doing.

pub

The pub keyword marks a function as visible when a zig file is pulled in via the import statement.

pub

break

A break is used to exit a loop early.

break

An interesting thing about break is that if you use it with a labeled block, you can use the break to return a value while also ending the block. This is what the prng function is using to generate the seed.

Blocks

while

A regular old while loop.

while

catch

You can use catch to set default values when something throws an error. You can also use catch to execute a block of code so that it gets executed when an error gets thrown. This is what happens when try is used. Try is sugar to actually do a catch on the error and immediately returning the error.

This is why the return type of the function is a union of error and the real return type.

catch

switch

The switch statement is quite nice with the fact that you don't need the break and the arrows to signify the blocks. The catch switch structure is a nice way of handling errors.

switch

continue

Continue can be used to jump back to the beginning of a loop.

continue

Other thoughts

Try statement

Big fan of the try statement and I find that it naturally fits the way I think about errors.

If Statement Binding

The binding that happens in an if statement is strange. I'm not sure why it exists yet.

Comparing Strings

You can't compare strings with equal signs. This is because strings of different lengths are considered different types. The types of a string have their size associated with them and their terminator. This is because a string is just an array and comparing arrays of 2 different lengths doesn't make sense.

prng Random Block

I'm not sure why the generation of the seed is inside it's own block for the prng. Generating the seed makes sense and you can do it outside the block. This code is also on ziglearn so it may be the idiomatic way to generate a seed. I wonder what gains the block gives you versus the straightforwardness of moving the seed generation outside of the block.

Catching Errors

I like the way the catch works. Having it be right on the erroring line and then dealing with it via ifs or switches is obvious and painless. I find this mixed with try to be a very natural way of dealing with errors.

If Statement for Errors

I would like to use the if statement error structure I've seen in the documentation but it makes it so that the error handling is after the success case. I'd rather deal with the error immediately and then have the success outside the if. This way errors don't causing nesting indentations.

One Line If Statement

The one line if statement does not look good. I really like the ternary style that you see in C.

getrandom

Why is getrandom getrandom and not getRandom?