Introduction to Zig
Abstract
- Why learn programming languages
- Set up development environment
- Zig Overview
- Free practice
- Unleash you on the rest of Zig Day attendees
Why Learn PLs?
- Look behind the curtain
- Anyone can create one
- Creators imprint upon thier languages
- New languages can teach you new ways of thinking
Why Learn Zig?
- Cross-compiling toolchain
- "Simple"
- Optimize for reading over writing
- Designed to build high quality software, not great for quick scripts
Toolchain
- Clang is embedded in the Zig compiler
- Zig can cross-compile C
- Zig can directly import C
- Zig build system is written in Zig
"Simple" Language
- Small enough to fit in your head
- "If everyone had their favourite feature from C++, we'd end up with C++" - Andrew Kelley
- Features tend to work nicely together
Programming Mindset
- Be explicit
- Leave trip wires to catch incorrectness
- Document assumptions using assertions
- Incorrect program should crash and provide information
Dev Environment
Hello World
- Create new directory
- Run `zig init`
- Inspect generated files
$ zig build run
$ zig build testInteger Overflow
const std = @import("std");
fn add(a: u8, b: u8) u8 {
return a + b;
}
pub fn main() void {
const b = add(200, 200);
// Note, Zig doesn't allow unused variables
// This says "yes I know, but do it anyways"
_ = b;
}
Save this to a file ending with .zig, then run:
$ zig run ./integer_overflow.zigZig Overview
Purpose
- Show you how the features layer
- Not exhaustive
- If you don't get something, that's fine, you'll get to revisit in detail later
Integers
// Signedness
const a: u32 = 42;
const b: i32 = -560;
// Arbitrary bit size
const c: u3 = 7;
// Wrapping and saturating operators
const d = a %+ 200;
const e = b |+ -5000;Functions
fn double(a: u32) u32 {
return 2 * a;
}
pub fn main() void {
// This won't compile. Return values
// MUST be handled somehow
double(4);
const c = double(2);
_ = c;
}If Else
fn my_func(flag: bool, num: u16) void {
if (flag) {
// Do thing if flag is true...
// Whatever is in the parenthesis MUST result to a boolean, integers
// and pointers do not implicitly convert, you must explicitly compare
// it.
} else if (num != 0) {
// This happens if ptr is null, and flag is false
} else {
// do other thing...
}
}If/Else as an Expression
fn special_case(n: u32) u32 {
// in C, this would be: (n % 2 == 0) ? 1 : n;
const a = if (n % 2 == 0) 1 else n;
return 2 * a;
}While Loop
pub fn main() void {
var acc: u32 = 0;
var i: u32 = 0;
while (i < 10) {
acc += i;
i += 1;
}
// Alternatively:
while (i < 20) : (i += 1) {
acc += i;
}
}
For Loop
var array1: [10]u8 = undefined;
var array2: [10]u8 = undefined;
fn main() void {
// Fixed range iteration
for (0..10) {
// ...
}
// Iterate array
for (array1) |value| {
// ^ "capture" value
}
// Get an index alongside value
for (array1, 0..) |value, i| {
// ^ Can be a different start offset
// Eg. 3 would effectively be 3..13
}
// Arrays/slices of the same length can be looped side-by-side
for (array1, array2) |value1, value2| {
// ...
}
}Loop as an Expression
var array: [5]u8 = .{1, 2, 3, 7, 5};
fn main() void {
// Find the index of the even number in the array
const index = for (array, 0..) |value, i| {
if (value % 2 == 0)
break i;
} else 0; // This happens if we never break from the loop
_ = index;
}Switch
fn is_printable(char: u8) bool {
switch (char) {
// Single match
0x21 => return true,
// List matches
0x20, 0x22 => return true,
// Exclusive range
0x21..0x30 => return true,
// Inclusive range
0x30...0x7E => return true,
// "default" in C
else => return false,
}
}Switch as an Expression
fn is_printable(char: u8) bool {
return switch (char) {
0x20 ... 0x7E => true,
else => false,
}
}Enums
// Types are values too
const Color = enum {
red,
blue,
green,
};
fn is_warm(color: Color) bool {
// .red is an "anonymous" enum literal. The compiler ensures that it's part
// of Color at compile-time
return color == .red;
}
// You can switch on enums
pub fn is_cool(color: Color) bool {
return switch(color) {
.green, .blue => true,
.red => false,
};
}Errors
// This is an "error set", it's like an enum, but is handled differently in
// different contexts.
const OpenError = error {
DoesntExist,
Denied,
};
fn open_file() OpenError!u32 {
if (some_flag) {
// Error literals start with "error."
return error.Denied;
}
// Just a value to fake a file handle
return 0;
}Errors Continued
// main can return errors too. anyerror is a special error set that's all error
// sets in the program combined into one.
pub fn main() anyerror!void {
// You can "catch" an error, and return a default value that matches the
// "value" type
const handle = open_file() catch 1;
// catch an error, capture the exact error in 'err', then return error from main
const handle = open_file() catch |err| return err;
// A shorter version of the above, very common:
const handle = try open_file();
// Big surprise, you can switch on errors:
const handle = open_file() catch |err| switch (err) {
error.DoesntExist => {
// do stuff
},
error.Denied => {
// do something else
},
};
}
Pointers
pub fn main() void {
var i: u32 = 4;
var buf: [80]u8 = undefined;
// pointer to single item
const j: *u32 = &i;
// pointer to many
const k: [*]u8 = &buf;
// optionality
const l: ?*i16 = null;
// sentinel, here is the equivalent of a C string literal:
const c_str: ?[*:0]const u8 = null;
// pointers can be dereferenced with .*:
const m = j.*;
}Optionals
// Optionality can be applied to any type, even non-pointers.
fn maybe_do_thing(num: ?u32) {
// optionals have to be unwrapped, the explicit way is:
if (num) |n| {
// do thing...
}
// If I KNOW it's not null, I can "dereference" it:
const m = num.?;
}Structures
// Bundle primitive types into a more complex type:
const Player = struct {
x: u32,
y: u32,
};
pub fn main() void {
// This is what it looks like to initialize a struct:
var main_player = Player{
.x = 2,
.y = 4,
};
// fields can be accessed with .
main_player.x = 2;
}
Methods
const Player = struct {
x: u32,
y: u32,
// A function nested in the struct, and takes the struct the first argument
// can be called as a method.
fn move_forward(player: *Player, count: u32) void {
player.y += count;
}
};
pub fn main() void {
// This is what it looks like to initialize a struct:
var main_player = Player{
.x = 2,
.y = 4,
};
main_player.move_forward(5);
// It's just syntactic sugar, equivalently:
Player.move_forward(&player, 5);
}comptime
const std = @import("std");
// Generics can be acheived with "comptime".
fn max(comptime T: type, a: T, b: T) T {
// Types are values, and can be used as such.
if (T == bool) {
return a or b;
} else if (a > b) {
return a;
} else {
return b;
}
}
pub fn main() void {
_ = max(u32, 1, 10);
_ = max(bool, true, true);
}
comptime
const std = @import("std");
// Functions can return types
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
// Type alias
const IndexList = List(u32);
fn fibonacci(index: u32) u32 {
if (index < 2) return index;
return fibonacci(index - 1) + fibonacci(index - 2);
}
pub fn main() void {
var buf: [10]u32 = undefined;
var list: IndexList = .{
.items = &buf,
.len = 0,
};
// Execution of a function at compile-time can be enforced:
const n = comptime fibonacci(100);
_ = n;
}Where to Go From Here
Zig Communities
- Zigcord
- Zig Embedded Group Discord
- Ziggit (forum)
- IRC
Resources
Congratulations, we all made it
Introduction to Zig
By Matt Knight
Introduction to Zig
- 11