Zig at a Glance — A Language a Day, Advent Calendar 2019 Day 22/24

Welcome to Day 22 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Zig programming language.

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 22 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Zig programming language.

Facts about the language

Some facts about the Zig programming language:

  • Aims for creating reliable and robust software (e. g., no hidden memory allocations)
  • Re-thinking of C without the pre-processor and macros
  • No hidden control flow (e. g., overriding operators)
  • Appeared in 2017
  • Website: ziglang.org

Installing and running Zig

You can build Zig from source files or install it using a package manager for your operating system. For Macs, the command is straightforward:

$ brew install zig

Zig is a compiled language; to compile a program, use the build-exe command:

$ zig build-exe helloworld.zig
$ ./helloworld

Alternative, compile and run in one go:

$ zig run helloworld.zig

Hello, World!

Let us see the two versions of the program. First, the one that prints to STDERR.

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

pub fn main() void {
    warn("Hello, World!\n");
}

The second program outputs to STDOUT.

const std = @import("std");

pub fn main() !void {
    const stdout_file = try std.io.getStdOut();
    try stdout_file.write("Hello, World!\n");
}

Notice that this time, you have to think about possible errors. First, there is a try when accessing the standard output. Second, when writing to it. As this version of the main function can end up with an error, its return type is !void, not void (we’ll return to it later).

Variables and constants

You define a constant with the const keyword, and variables with var. The type specifier is expected after a semicolon.

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

pub fn main() void {
    const name = "John";
    var age: i32 = 32;

    warn("{} is {} years old.\n", name, age);
}

In this example, you can also see how you can build the output strings using the printf-like string interpolation.

Functions

Functions are declared with the fn keyword. Notice that in the case of main, you have to additionally mark it as pub, otherwise it will be left private. Public functions are visible when you @import the file containing them.

The type of the arguments is specified after the colon as in variable declaration.

Here is an example of a function that does string concatenation and returns a new string.

const warn = @import("std").debug.warn;
const fmt = @import("std").fmt;

var buf: [100]u8 = undefined;

fn greet(name: []const u8) ![]const u8 {
    return fmt.bufPrint(buf[0..], "Hello, {}!", name);
}

pub fn main() void {
    warn("{}\n", greet("John"));
}

To concatenate strings, you have to provide the program with a buffer big enough to keep the string.

This is a point where an error can happen. If you set the buffer size fixed, you can get an error when you pass a long name. Try, for example, making the string much longer in the main function. The program compiles but ends with a runtime error: error.BufferTooSmall.

Notice that in the current solution, the buffer is reused every time the function is called (and thus, the previous value is lost). Alternatively, you can compute the length needed at runtime and allocate the space, which is another source of a potential error (out of memory).

The return type of the function is ![]const u8, where an exclamation mark indicates that the function can end up with an error.

A Factorial example

Here is a program that computes and prints factorials:

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

fn factorial(n: i32) i32 {
    if (n < 2) return 1
    else return n * factorial(n - 1);
}

pub fn main() void {
    warn("{}\n", factorial(1)); // 1
    warn("{}\n", factorial(5)); // 120
    warn("{}\n", factorial(7)); // 5040
}

Compile-time evaluation

When it is possible to evaluate an expression at compile time, you can mark it with comptime. For example, it is possible to modify the factorial example to precompute all the values.

const warn = @import("std").debug.warn;
const assert = @import("std").debug.assert;

fn factorial(n: i32) i32 {  
    assert(n > 0);
    if (n < 2) return 1
    else return n * factorial(n - 1);
}

pub fn main() void {
    warn("{}\n", comptime factorial(1)); // 1
    warn("{}\n", comptime factorial(5)); // 120
    warn("{}\n", comptime factorial(7)); // 5040
}

Additionally, this program checks if the argument passed to the factorial function is positive. You can try passing a negative number from the main function, and you will get a compile, not a runtime error.

Catching errors

In some of the previous examples, we saw an exclamation sign in the return type of the functions. Let us dig here a bit deeper by looking at the following example.

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

fn div(n: i32) !i32 { // Error Union Type
    if (n == 0) return error.BadValue
    else return @divFloor(42, n);
}

pub fn main() !void {
    var x: i32 = 2;
    warn("42 / {} = {}\n", x, try div(x));

    x = 0;
    warn("42 / {} = {}\n", x, try div(x));

    warn("Done.\n");
}

In this program, the div function can either return an integer or an error. The !i32 construct is called an error union type. The if check decides whether it is possible to compute a result; otherwise it returns an error.

In the main function, the call of the div function is prefixed with try, which is transparent for the result value when there is no error, and propagates an error when it happens.

You may confirm that the error happens at runtime, and the program aborts at the second call when the division by zero happens:

$ zig run div-zero.zig
42 / 2 = 21
error: BadValue
/Users/ash/test/advent-2019/Zig/div-zero.zig:4:17: 0x1036784d8 in _div (run.o)
    if (n == 0) return error.BadValue
                ^
/Users/ash/test/advent-2019/Zig/div-zero.zig:13:31: 0x1036782f5 in _main.0 (run.o)
    warn("42 / {} = {}\n", x, try div(x));

Notice that if you remove try, then the output will be completely different, and the program continues after the error:

$ zig run div-zero.zig
42 / 2 = 21
42 / 0 = error.BadValue
Done.

Structs

Zig offers C-like structs, which can also have methods, like in C++. In the following program, we create a Person structure, then a variable of this type, and then call the info method on the variable.

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

const Person = struct {
    name: []const u8,
    age: i32,

    pub fn info(self: Person) void {
        warn("{} is {} years old.\n", self.name, self.age);
    }
};

pub fn main() void {
    const p = Person {
        .name = "John",
        .age = 42,
    };

    p.info(); // John is 42 years old.
}

As you can see, the method here is a function that receives a Person as its first argument.

Get more

In this article, I only mentioned a part of what Zig gives to the developer for creating reliable software. I would recommend you to examine the documentation to get more about what the language is.

In the recent version of Zig, 0.5.0, the async/await functionality was added. Explore the dedicated repository to see a number of examples.

The source codes for today’s blog post are published on GitHub.

Next: Day 23. Io

Leave a Reply

Your email address will not be published. Required fields are marked *