A half-hour to learn Zig
This is inspired by https://fasterthanli.me/blog/2020/a-half-hour-to-learn-rust/
Basics
the command zig run my_code.zig
will compile and immediately run your Zig program. Each of these cells contains a zig program that you can try to run (some of them contain compile-time errors that you can comment out to play with)
You'll want to declare a main() function to get started running code.
This program does almost nothing:
// comments look like this and go to the end of the line pub fn main() void {}
You can import from the standard library by using the @import
builtin and assigning the namespace to a const value. Almost everything in zig must be explicitly assigned its identifier.
const std = @import("std"); pub fn main() void { std.debug.print("hello world!\n", .{}); }
var
declares a variable, in most cases you should declare the variable type.
const std = @import("std"); pub fn main() void { var x: i32 = 47; // declares "x" of type i32 to be 47. std.debug.print("x: {}\n", .{x}); }
const
declares that a variable's value is immutable.
pub fn main() void { const x: i32 = 47; x = 42; // error: cannot assign to constant }
Zig is very picky and will NOT let you shadow identifiers from an outside scope, to keep you from being confused:
const x: i32 = 47; pub fn main() void { var x: i32 = 42; // error: redefinition of 'x' }
Constants in the global scope are by default compile-time "comptime" values, and if you omit the type they are comptime typed and can turn into runtime types for your runtime values.
const x: i32 = 47; const y = -47; // comptime integer. pub fn main() void { var a: i32 = y; // comptime constant coerced into correct type var b: i64 = y; // comptime constant coerced into correct type var c: u32 = y; // error: cannot cast negative value -47 to unsigned integer }
You can explicitly choose to leave it undefined if it will get set later, but zig will error if you try to use it.
pub fn main() void { var x: i32 = undefined; foo(x); // error: use of undeclared identifier 'foo' }
In some cases, zig will let you omit the type information if it can figure it out.
const std = @import("std"); pub fn main() void { var x: i32 = 47; var y: i32 = 47; var z = x + y; // declares z and sets it to 94. std.debug.print("z: {}\n", .{z}); }
But be careful, integer literals are comptime-typed, so this won't work:
pub fn main() void { var x = 47; // error: variable of type 'comptime_int' must be const or comptime }
Functions
Here's a function (foo
) that returns nothing. The pub
keyword means that the function is exportable from the current scope, which is why main must be pub
. You call functions just as you would in most programming languages:
const std = @import("std"); fn foo() void { std.debug.print("foo!\n", .{}); //optional: return; } pub fn main() void { foo(); }
Here's a function that returns an integer value:
const std = @import("std"); fn foo() i32 { return 47; } pub fn main() void { var result = foo(); std.debug.print("foo: {}\n", .{result}); }
Zig won't let you ignore return values for functions:
fn foo() i32 { return 47; } pub fn main() void { foo(); // error: expression value is ignored }
but you can if you assign it to the throw-away _
.
fn foo() i32 { return 47; } pub fn main() void { _ = foo(); }
You can make a function that can take a parameter by declaring its type:
const std = @import("std"); fn foo(x: i32) void { std.debug.print("foo param: {}\n", .{x}); } pub fn main() void { foo(47); }
Structs
structs are declared by assigning them a name using the const keyword, they can be assigned out of order, and they can be used by dereferencing with the usual dot syntax.
const std = @import("std"); const Vec2 = struct{ x: f64, y: f64 }; pub fn main() void { var v = Vec2{.y = 1.0, .x = 2.0}; std.debug.print("v: {}\n", .{v}); }
structs can have default values; structs can also be anonymous, and can coerce into another struct so long as all of the values can be figured out:
const std = @import("std"); const Vec3 = struct{ x: f64 = 0.0, y: f64, z: f64 }; pub fn main() void { var v: Vec3 = .{.y = 0.1, .z = 0.2}; // ok var w: Vec3 = .{.y = 0.1}; // error: missing field: 'z' std.debug.print("v: {}\n", .{v}); }
Enums
Enums are declared by assigning the group of enums as a type using the const keyword.
Note:
- In some cases you can shortcut the name of the Enum.
- You can set the value of an Enum to an integer, but it does not automatically coerce, you have to use
@enumToInt
or@intToEnum
to do conversions.
const std = @import("std"); const EnumType = enum{ EnumOne, EnumTwo, EnumThree = 3 }; pub fn main() void { std.debug.print("One: {}\n", .{EnumType.EnumOne}); std.debug.print("Two?: {}\n", .{EnumType.EnumTwo == .EnumTwo}); std.debug.print("Three?: {}\n", .{@enumToInt(EnumType.EnumThree) == 3}); }
Arrays and Slices
zig has Arrays, which are contiguous memory with compile-time known length. You can initialize them by declaring the type up front and providing the list of values. You can access the length with the len
field of the array.
Note:
- Arrays in zig are zero-indexed.
const std = @import("std"); pub fn main() void { var array: [3]u32 = [3]u32{47, 47, 47}; // also valid: // var array = [_]u32{47, 47, 47}; var invalid = array[4]; // error: index 4 outside array of size 3. std.debug.print("array[0]: {}\n", .{array[0]}); std.debug.print("length: {}\n", .{array.len}); }
zig also has slices, which are have run-time known length. You can construct slices from arrays or other slices using the slicing operation. Similarly to arrays, slices have a len field which tells you its length.
Note:
- The interval parameter in the slicing operation is open (non-inclusive) on the big end.
Attempting to access beyond the range of the slice is a runtime panic (this means your program will crash).
const std = @import("std"); pub fn main() void { var array: [3]u32 = [_]u32{47, 47, 47}; var slice: []u32 = array[0..2]; // also valid: // var slice = array[0..2]; var invalid = slice[3]; // panic: index out of bounds std.debug.print("slice[0]: {}\n", .{slice[0]}); std.debug.print("length: {}\n", .{slice.len}); }
string literals are null-terminated utf-8 encoded arrays of const u8
bytes.
Unicode characters are only allowed in string literals and comments.
Note:
- length does not include the null termination (officially called "sentinel termination")
- it's safe to access the null terminator.
- indices are by byte, not by unicode glyph.
const std = @import("std"); const string = "hello 世界"; const world = "world"; pub fn main() void { var slice: []const u8 = string[0..5]; std.debug.print("string {}\n", .{string}); std.debug.print("length {}\n", .{world.len}); std.debug.print("null {}\n", .{world[5]}); std.debug.print("slice {}\n", .{slice}); std.debug.print("huh? {}\n", .{string[0..7]}); }
const arrays can be coerced into const slices.
const std = @import("std"); fn foo() []const u8 { // note function returns a slice return "foo"; // but this is a const array. } pub fn main() void { std.debug.print("foo: {}\n", .{foo()}); }
Control structures
Zig gives you an if statement that works as you would expect.
const std = @import("std"); fn foo(v: i32) []const u8 { if (v < 0) { return "negative"; } else { return "non-negative"; } } pub fn main() void { std.debug.print("positive {}\n", .{foo(47)}); std.debug.print("negative {}\n", .{foo(-47)}); }
as well as a switch statement
const std = @import("std"); fn foo(v: i32) []const u8 { switch (v) { 0 => return "zero", else => return "nonzero" } } pub fn main() void { std.debug.print("47 {}\n", .{foo(47)}); std.debug.print("0 {}\n", .{foo(0)}); }
Zig provides a for-loop that works only on arrays and slices.
const std = @import("std"); pub fn main() void { var array = [_]i32{47, 48, 49}; for (array) | value | { std.debug.print("array {}\n", .{value}); } for (array) | value, index | { std.debug.print("array {}:{}\n", .{index, value}); } var slice = array[0..2]; for (slice) | value | { std.debug.print("slice {}\n", .{value}); } for (slice) | value, index | { std.debug.print("slice {}:{}\n", .{index, value}); } }
Zig provides a while-loop that also works as you might expect:
const std = @import("std"); pub fn main() void { var array = [_]i32{47, 48, 49}; var index: u32 = 0; while (index < 2) { std.debug.print("value: {}\n", .{array[index]}); index += 1; } }
Error handling
Errors are special union types, you denote that a function can error by prepending !
to the front. You throw the error by simply returning it as if it were a normal return.
const MyError = error{ GenericError, // just a list of identifiers, like an enum. OtherError }; pub fn main() !void { return MyError.GenericError; }
If you write a function that can error, you must decide what to do with it when it returns. Two common options are try
which is very lazy, and simply forwards the error to be the error for the function. catch
explicitly handles the error.
try
is just sugar forcatch | err | {return err}
const std = @import("std"); const MyError = error{ GenericError }; fn foo(v: i32) !i32 { if (v == 42) return MyError.GenericError; return v; } pub fn main() !void { // catch traps and handles errors bubbling up _ = foo(42) catch |err| { std.debug.print("error: {}\n", .{err}); }; // try won't get activated here. std.debug.print("foo: {}\n", .{try foo(47)}); // this will ultimately cause main to print an error trace and return nonzero _ = try foo(42); }
You can also use if to check for errors.
const std = @import("std"); const MyError = error{ GenericError }; fn foo(v: i32) !i32 { if (v == 42) return MyError.GenericError; return v; } // note that it is safe for wrap_foo to not have an error ! because // we handle ALL cases and don't return errors. fn wrap_foo(v: i32) void { if (foo(v)) | value | { std.debug.print("value: {}\n", .{value}); } else | err | { std.debug.print("error: {}\n", .{err}); } } pub fn main() void { wrap_foo(42); wrap_foo(47); }
A taste of metaprogramming
Zig's metaprogramming is driven by two basic concepts:
- Types are valid values at compile-time
- most runtime code will also work at compile-time.
- anonymous struct coercion is compile-time duck-typed.
- the zig standard library gives you tools to perform compile-time reflection.
Here's an example of multiple dispatch:
const std = @import("std"); fn foo(x : anytype) @TypeOf(x) { // note that this if statement happens at compile-time, not runtime. if (@TypeOf(x) == i64) { return x + 2; } else { return 2 * x; } } pub fn main() void { var x: i64 = 47; var y: i32 = 47; std.debug.print("i64-foo: {}\n", .{foo(x)}); std.debug.print("i32-foo: {}\n", .{foo(y)}); }
Here's an example of generic types:
const std = @import("std"); fn Vec2Of(comptime T: type) type { return struct{ x: T, y: T }; } const V2i64 = Vec2Of(i64); const V2f64 = Vec2Of(f64); pub fn main() void { var vi = V2i64{.x = 47, .y = 47}; var vf = V2f64{.x = 47.0, .y = 47.0}; std.debug.print("i64 vector: {}\n", .{vi}); std.debug.print("i32 vector: {}\n", .{vf}); }
From these concepts you can build very powerful generics!
Coda
That's it! Now you know a fairly decent chunk of zig.
For more details, check the latest documentation: https://ziglang.org/documentation/master/
or for a less half-baked tutorial, go to: https://ziglearn.org/
from Hacker News https://ift.tt/3pH5kIT
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.