Skip to main content

· 5 min read
Sobeston

We are going to make a program that randomly picks a number from 1 to 100 and asks us to guess it, telling us if our number is too big or two small.

Getting a Random Number

As Zig does not have a runtime, it does not manage a PRNG (pseudorandom number generator) for us. This means that we'll have to create our PRNG and initialise it with a source of entropy. Let's start with a file called a_guessing_game.zig.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

Let's initialise std.rand.DefaultPrng with a 64 bit unsigned integer (u64). Our rand here allows us to access many useful utilities for our PRNG. Here we're asking our PRNG for a random number from 1 to 100, however, if our PRNG is initialised with the same number every time our program will always print out the same number.

    var prng = std.rand.DefaultPrng.init(1625953);
const rand = prng.random();

try stdout.print(
"not-so random number: {}\n",
.{rand.intRangeAtMost(u8, 1, 100)},
);

For a good source of entropy, it is best to initialise our PRNG with random bytes provided by the OS. Let's ask the OS for some. As Zig doesn't let us declare a variable without a value we've had to give our seed variable the value of undefined, which is a special value that coerces to any type. The function std.posix.getrandom takes in a slice of bytes, where a slice is a pointer to a buffer whose length is known at run time. Because of this we've used std.mem.asBytes to turn our pointer to a u64 into a slice of bytes. If getrandom succeeds it will fill our seed variable with a random value which we can then initialise the PRNG with.

    var seed: u64 = undefined;
try std.posix.getrandom(std.mem.asBytes(&seed));

var prng = std.rand.DefaultPrng.init(seed);
const rand = prng.random();

Taking User Input

Let's start here, where our program already has a random secret value which we must guess.

const std = @import("std");

pub fn main() !void {
var seed: u64 = undefined;
try std.posix.getrandom(std.mem.asBytes(&seed));

var prng = std.rand.DefaultPrng.init(seed);
const rand = prng.random();

const target_number = rand.intRangeAtMost(u8, 1, 100);

As we'll be printing and taking in user input until the correct value is guessed, let's start by making a while loop with stdin and stdout. Note how we've obtained an stdin reader.

    while (true) {
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut().writer();

To get a line of user's input, we have to read stdin until we encounter a newline character, which is represented by \n. What is read will need to be copied into a buffer, so here we're asking readUntilDelimiterAlloc to allocate a buffer up to 8KiB using std.heap.page_allocator until it reaches the \n character.

        const bare_line = try stdin.readUntilDelimiterAlloc(
std.heap.page_allocator,
'\n',
8192,
);
defer std.heap.page_allocator.free(bare_line);

Because of legacy reasons newlines in many places in Windows are represented by the two-character sequence \r\n, which means that we must strip \r from the line that we've read. Without this our program will behave incorrectly on Windows.

        const line = std.mem.trim(u8, bare_line, "\r");

Guessing

Let's continue from here. We're expecting the user to input an integer number here, so the next step is to parse a number from line.

const std = @import("std");

pub fn main() !void {
var seed: u64 = undefined;
try std.posix.getrandom(std.mem.asBytes(&seed));

var prng = std.rand.DefaultPrng.init(seed);
const rand = prng.random();

const target_number = rand.intRangeAtMost(u8, 1, 100);

while (true) {
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut().writer();

const bare_line = try stdin.readUntilDelimiterAlloc(
std.heap.page_allocator,
'\n',
8192,
);
defer std.heap.page_allocator.free(bare_line);

const line = std.mem.trim(u8, bare_line, "\r");

This can be achieved by passing the buffer to std.fmt.parseInt, where the last parameter is the base of the number in the string. So far we've only handled errors with try, which returns the error if encountered, but here we'll want to catch the error so that we can process it without returning it. If there's an error we'll print a friendly error message and continue, so that the user can re-enter their number.

        const guess = std.fmt.parseInt(u8, line, 10) catch |err| switch (err) {
error.Overflow => {
try stdout.writeAll("Please enter a small positive number\n");
continue;
},
error.InvalidCharacter => {
try stdout.writeAll("Please enter a valid number\n");
continue;
},
};

Now all we have to do is decide what to do with the user's guess. It's important to leave the loop using break when the user makes a correct guess.

        if (guess < target_number) try stdout.writeAll("Too Small!\n");
if (guess > target_number) try stdout.writeAll("Too Big!\n");
if (guess == target_number) {
try stdout.writeAll("Correct!\n");
break;
}

Let's try playing our game.

$ zig run a_guessing_game.zig 
45
Too Big!
20
Too Small!
25
Too Small!
32
Too Small!
38
Too Small!
41
Too Small!
43
Too Small!
44
Correct!

· 4 min read
Sobeston

Here we're going to walk through writing a program that takes a measurement in fahrenheit as its argument, and prints the value in celsius.

Getting Arguments

Let's start by making a file called fahrenheit_to_celsius.zig. Here we'll again obtain a writer to stdout like before.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

Now let's obtain our process' arguments. To get arguments in a cross-platform manner we will have to allocate memory, which in idiomatic Zig means the usage of an allocator. Here we'll pass in the std.heap.page_allocator, which is the most basic allocator that the standard library provides. This means that the argsAlloc function will use this allocator when it allocates memory. This has a try in front of it as memory allocation may fail.

    const args = try std.process.argsAlloc(std.heap.page_allocator);

The argsAlloc function, after unwrapping the error, gives us a slice. We can iterate over this with for, "capturing" the values and indexes. Let's use this to print all of the arguments.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const args = try std.process.argsAlloc(std.heap.page_allocator);

for (args, 0..) |arg, i| {
try stdout.print("arg {}: {s}\n", .{ i, arg });
}
}

This program will print something like this when run with zig run fahrenheit_to_celsius.zig.

arg 0: /home/sobe/.cache/zig/o/b947fb3eac70ec0595800316064d88dd/fahrenheit_to_celsius

For us the 0th argument is not what we want - we want the 1st argument, which is provided by the user. There are two ways we can provide our program more arguments. The first is via build-exe.

zig build-exe fahrenheit_to_celsius.zig
./fahrenheit_to_celsius first_argument second_argument ...
# windows: .\fahrenheit_to_celsius first_argument second_argument ...

We can also pass in arguments with zig run as follows.

zig run fahrenheit_to_celsius.zig -- first_argument second_argument ...

Let's have our program skip the 0th argument, and make sure that there's a first argument.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const args = try std.process.argsAlloc(std.heap.page_allocator);

if (args.len < 2) return error.ExpectedArgument;

for (args, 0..) |arg, i| {
if (i == 0) continue;
try stdout.print("arg {}: {s}\n", .{ i, arg });
}
}

Finally, now that we've gotten the arguments, we should deallocate the memory that we allocated in order to obtain the arguments. Here we're introducing the defer statement. What follows a defer statement will be executed when the current function is returned from. The usage here means that we can be sure our args' memory is freed when main is returned from.

    const args = try std.process.argsAlloc(std.heap.page_allocator);
defer std.process.argsFree(std.heap.page_allocator, args);

Performing Conversion

Now that we know how to get the process' arguments, let's start performing the conversion. Let's start from here.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

const args = try std.process.argsAlloc(std.heap.page_allocator);
defer std.process.argsFree(std.heap.page_allocator, args);

if (args.len < 2) return error.ExpectedArgument;

The first step is to turn our argument string into a float. The standard library contains such a utility, where the first argument is the type of float returned. This function fails if it is provided a string which cannot be turned into a float.

    const f = try std.fmt.parseFloat(f32, args[1]);

We can convert this to celsius as follows.

    const c = (f - 32) * (5.0 / 9.0);

And now we can print the value.

    try stdout.print("{}c\n", .{c});

However this will give us an ugly output, as the default float formatting gives us scientific form.

$ zig run fahrenheit_to_celsius.zig -- 100
3.77777786e+01c

By changing the format specifier from {} to {d}, we can print in decimal form. We can also reduce the precision of the output by using {d:.x}, where x is the amount of decimal places.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

const args = try std.process.argsAlloc(std.heap.page_allocator);
defer std.process.argsFree(std.heap.page_allocator, args);

if (args.len < 2) return error.ExpectedArgument;

const f = try std.fmt.parseFloat(f32, args[1]);
const c = (f - 32) * (5.0 / 9.0);
try stdout.print("{d:.1}c\n", .{c});
}

This yields a much more friendly output.

zig run fahrenheit_to_celsius.zig -- 100
37.8c

· 4 min read
Sobeston

Let's start playing with Zig by solving a problem together.

Fizz buzz is a game where you count upwards from one. If the current number isn't divisible by five or three, the number is said. If the current number is divisible by three, "Fizz" is said; if the number is divisible by five, "Buzz" is said. And if the number is divisible by both three and five, "Fizz Buzz" is said.

Starting

Let's make a new file called fizz_buzz.zig and fill it with the following code. This provides us with an entry point and a way to print to the console. For now we will take for granted that our stdout writer works.

const std = @import("std");

pub fn main() !void {
const stdout = std.io.getStdOut().writer();

}

Here, const is used to store the value returned by getStdOut().writer(). We may use var to declare a variable instead; const denotes immutability. We'll want a variable that stores what number we're currently at, so let's call it count and set it to one.

    const stdout = std.io.getStdOut().writer();
var count = 1;

In Zig there is no default integer type for your programs; what languages normally call "int" does not exist in Zig. What we've done here is made our count variable have the type of comptime_int. As the name suggests, these integers may only be manipulated at compile time which renders them useless for our uses. When working with integers in Zig you must choose the size and signedness of your integers. Here we'll make count an unsigned 8-bit integer, where the u in u8 means unsigned, and i is for signed.

    const stdout = std.io.getStdOut().writer();
var count: u8 = 1;

What we'll do next is introduce a loop, from 1 to 100. This while loop is made up of three components: a condition, a continue expression, and a body, where the continue expression is what is executed upon continuing in the loop (whether via the continue keyword or otherwise).

    var count: u8 = 1;
while (count <= 100) : (count += 1) {

}

Here we'll print all numbers from 1 to 100 (inclusive). The first argument of print is a format string and the second argument is the data. Our usage of print here outputs the value of count followed by a newline.

    var count: u8 = 1;
while (count <= 100) : (count += 1) {
try stdout.print("{}\n", .{count});
}

Now we can test count for being multiples of three or five, using if statements. Here we'll introduce the % operator, which performs modulus division between a numerator and denominator. When a % b equals zero, we know that a is a multiple of b.

    var count: u8 = 1;
while (count <= 100) : (count += 1) {
if (count % 3 == 0 and count % 5 == 0) {
try stdout.writeAll("Fizz Buzz\n");
} else if (count % 5 == 0) {
try stdout.writeAll("Buzz\n");
} else if (count % 3 == 0) {
try stdout.writeAll("Fizz\n");
} else {
try stdout.print("{}\n", .{count});
}
}

Modulus division is more complicated with a signed numerator.

Using a Switch

We can also write this using a switch over an integer. Here we're using @intFromBool which converts bool values into a u1 value (i.e. a 1 bit unsigned integer). You may notice that we haven't given div_5 an explicit type - this is because it is inferred from the value that is assigned to it. We have however given div_3 a type; this is as integers may widen to larger ones, meaning that they may coerce to larger integer types providing that the larger integer type has at least the same range as the smaller integer type. We have done this so that the operation div_3 * 2 + div_5 provides us a u2 value, or enough to fit two booleans.

pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var count: u8 = 1;

while (count <= 100) : (count += 1) {
const div_3: u2 = @intFromBool(count % 3 == 0);
const div_5 = @intFromBool(count % 5 == 0);

switch (div_3 * 2 + div_5) {
0b10 => try stdout.writeAll("Fizz\n"),
0b11 => try stdout.writeAll("Fizz Buzz\n"),
0b01 => try stdout.writeAll("Buzz\n"),
0b00 => try stdout.print("{}\n", .{count}),
}
}
}

We can rewrite the switch value to use bitwise operations. This is equivalent to the operation performed above.

switch (div_3 << 1 | div_5) {

Wrapping Up

Here you've successfully written two Fizz Buzz programs using some of Zig's basic arithmetic and control flow primitives. Hopefully you feel introduced to the basics of writing Zig code. Don't worry if you didn't understand it all.