Video summary
Bu video, Zig programlama dilinin temel özelliklerini ve benzersiz yeteneklerini, "Ziglings" projesindeki kırık kod örneklerinin düzeltilmiş hallerini inceleyerek tanıtmaktadır. İzleyicilerin C, Rust veya Go gibi dillerle ilgili bir arka planı olduğu varsayılmakta olup, video Zig dilinin standart modülünü nasıl içe aktardığını ve `main` fonksiyonunun yapısını açıklamaya başlar. İlk örneklerde değişken tanımları, sabit sayılar (örneğin pi sayısı) ve debug modülü üzerinden çıktı alma gibi temel işlemler gösterilirken; Zig'in parametre varsayılan değerleri desteklememesi nedeniyle boş yapı literallerinin bile fonksiyon çağrılarında zorunlu olduğu vurgulanır. Ayrıca video, dizi kopyalama yerine `double plus` ve `double asterisk` operatörlerini kullanarak dizileri birleştirme veya tekrar etme yöntemlerini anlatarak dilin bellek yönetimi açısından sunduğu esneklikten bahseder.
Video ilerledikçe Zig'in kontrol akışı yapılarını ele alırken, C'deki ternary operatöre benzer şekilde `if` ifadesinin nasıl kullanılabildiği ve döngülerde post-iterasyon ifadelerinin (`while`) işlevi açıklanır. Özellikle hata yönetimi üzerine yoğunlaşarak "error set" türlerinin küresel olarak eşleşen isimlere sahip olması, bu hataların `error union` ile birleştirilmesi ve `catch`, `try` gibi operatörlerin nasıl çalıştığı detaylandırılır. Bu bölümde ayrıca `defer` ifadesinin fonksiyonun çıkışında ertelenmiş işlemlerin (örn. temizlik kodu) otomatik olarak çalıştırılmasını sağladığı, bununla birlikte hata durumunda çalışan "error defer" mekanizması da örneklerle somutlaştırılır.
Sonuç bölümünde ise Zig'in derleme zamanı yetenekleri ve ileri düzey yapıları öne çıkarılarak; `comptime` değişkenlerinin derleme sırasında değerlendirilmesi, many-item pointer'ların indekslenmesi ve birleşik türlerin (union) nasıl işlendiği anlatılır. Video ayrıca switch ifadelerinin C'den farklı olarak düşüş yapmaması (`break` gerektirmemesi), yapı içindeki fonksiyon üyelerinin çağrılma biçimleri, opsiyonel tiplerin kullanımı ve hatta C kodunun Zig ile nasıl içe aktarılması gibi konulara değinerek dilin hem güvenliğini hem de esnekliğini bir arada sunan kapsamlı bir ekosistem olduğunu özetler. İzleyicilere bu hızlı taramadan ziyade, kendi başlarına Ziglings alıştırmalarını çözerek dili daha derinden öğrenmeleri tavsiye edilirken; video sonunda thread yönetimi ve test blokları gibi konularla dilin modern programlama ihtiyaçlarını karşılamaya yönelik araç setini tamamlar.
Read the full video transcript
This video is going to introduce the Zig
programming language by walking through
small code exercises from the Ziglings
project.
We'll assume the viewer has reasonable
familiarity with C or other similar
languages such as C++, Rust, Go, or
Odin.
If you're new to this kind of
programming, you may want to first check
out my intro to Odin video series linked
below.
The Ziglings exercises present broken
code examples that need fixes to pass
their tests, but here we'll present just
completed solutions rather than focus on
the particular problems being solved and
the logic of their solutions, the video
commentary and the code comments focus
just on the Zig language features
introduced by each exercise.
Also, understand that we won't cover
every single Ziglings exercise. Some
exercises are skipped because they are
redundant and several others are skipped
because they cover async, a feature that
isn't yet available in the main Zig
compiler.
Also, we will skip over most of the
exercises that focus on usage of the
standard libraries such as reading and
writing files.
I strongly recommend at some point
working through the Ziglings exercises
yourself, which generally takes several
hours or more.
Hopefully, this video gives you an easy
quick survey of Zig's unique features,
but you'll almost certainly understand
and retain them far better after you get
your hands dirty.
Lastly, before getting started, this
video will probably feel like a big list
of facts even more so than other videos.
So, it might be difficult to maintain
full attention if you attempt to watch
it in just one sitting.
Instead, you'll probably want to watch
it in chunks.
So, let's start with exercise three.
First, at the top, we're importing the
standard module with the import
function. The function returns the
module as a struct value, which we
assign to a constant we name STD.
By the way, the at symbol indicates that
this is a built-in function.
Then, we have the definition of the main
function, which is marked as public and
returns void, meaning it returns
nothing.
In the function, we're first declaring a
local variable n with type U8, meaning
an 8-bit unsigned integer.
We also declare a local constant named
pi with type U32, meaning a 32-bit
unsigned integer, and another local
constant named negative 11 with type I8,
meaning an 8-bit signed integer.
The last line calls the print function,
which is a member of the debug module,
which itself is included as a member of
the standard module. So, we can access
it through the STD constant we created
at the top of the file.
The print function takes two arguments,
a string and a value of any struct type.
In this case, we're passing an anonymous
struct denoted by the dot before the
opening curly brace.
Inside the curly braces, we have three
values.
And because these values are not given
member names, they are treated like
positional values in the struct for
indexes 0, 1, and 2.
What happens in the print function then
is that introspection is used to get the
members of the struct, and then the
values of these members are interpolated
into the string replacing the curly
braces.
So, this print call effectively outputs
the value of n, then a space, then the
value of pi, then another space,
negative 11, and lastly a new line.
Next, let's look at exercise five. After
importing the standard module like
before, we're also aliasing the assert
function to a constant in this file.
Inside main, we are creating two arrays
of U8 values.
The square brackets of an array literal
contain the size of the array, though
for these two arrays, we're using an
underscore to indicate that the size
should be inferred from the number of
elements in the curly braces.
So, both of these arrays have a size of
two.
In the next line, the double plus
operator is used to concatenate the two
arrays into a new array of length four.
The double asterisk operator
concatenates multiple instances of its
left operand the number of times
specified by its right operand. So, in
this case, three instances of the U8
array are concatenated together creating
an array of 12 U8s.
A few lines down, a for loop iterates
over every element of the array
assigning the element to a variable n in
each iteration.
Lastly, you may have noticed that many
of the print calls in this example pass
an empty anonymous struct literal.
Despite being empty, the struct is still
necessary because Zig does not support
default parameter values or variadic
functions. So, the print function must
always have a struct argument.
In exercise six, a string is assigned to
a local constant, and then when the
index operator is used on the string, we
get back a U8.
So, this assigns the U8 value at index
four of the string to the local constant
d.
Like with arrays, we can use the double
plus and double asterisk operators to
concatenate and repeat strings.
In the print call at the end, the U and
S in the curly braces indicate how the
values should be formatted. A U
indicates unsigned, and an S indicates
string.
In exercise seven, the double backslash
syntax indicates the start of a
multi-line string literal.
A multi-line literal runs to the end of
the line, and any successive lines
beginning with double backslash become
part of the same multi-line string.
So, this example has one multi-line
string literal spread across three
lines.
In exercise 10, an if is used as an
expression rather than a statement. This
is basically equivalent to the ternary
operator in C.
If the condition evaluates true, then
the first expression is evaluated.
Otherwise, if false, only the second is
evaluated.
So, here because discount is true, price
will be assigned 17.
In exercise 12, a while loop is given a
post-iteration expression denoted by the
colon.
This while loop will iterate as long as
n is less than 1,000, and after each
iteration, n is multiplied by two.
In exercise 16, the for loop here
iterates over both an array plus a range
in tandem.
The range denoted by the double dots
starts at zero, and the end of the range
is left inferred from context.
Because the length of the array and
range must match, the end of the range
is inferred to be the length of the
array.
Also, inside the loop here, the built-in
function int cast is used to cast from
one integer type to another.
The target integer type returned by int
cast is inferred from the calling
context. So, here because the assignment
target is a U32, this call returns a
U32.
In exercise 21, an error set type is
defined and assigned to the constant my
number error.
Like an enum, an error set is composed
of named members, but the names of error
set members are mapped to globally
unique IDs such that, say, the name foo
in one error set is considered
equivalent to the name foo from any
other error set,
which is not the case with enums. A foo
member of one enum would be totally
different from a foo member of a
different enum.
The number fail function defined in this
exercise returns the my number error
type, meaning it must return one of the
members of the error set.
In the next exercise 22, the type of the
first local variable is an error union
as denoted by the exclamation mark.
An error union combines an error set on
the left and a so-called payload type on
the right, which can be any kind of
type.
In this case, the union is between my
number error as the error set and U8 as
the payload.
What this means is that the variable can
be assigned any value of either type,
either any member of the my number error
error union or any U8 value.
So, in initialization, we assign the
variable the U8 value five,
but in the next line, we assign the
variable the my number error value too
small.
In exercise 23, the catch operator is
used, which takes an error union value
on the left and a value of its payload
type on the right.
If the error union value is a value of
its error set, then the catch evaluates
and returns the right operand.
Otherwise, if the error union is a value
of its payload type, the catch directly
returns the left payload without
evaluating the right operand.
In this example, the left operand is a
call to a function add 20, which returns
the error union of my number error and
U32.
The first call, which is passed 44, will
return 64, and so the catch directly
returns this value.
In the next line, though, the add 20
call is passed four, in which case it
returns a too small error, and so the
catch evaluates and returns the right
operand expression, which is the value
22.
In exercise 24, you can ignore most of
the code. The part to focus on here is
that the catch operator can capture the
error value from its left operand to be
used in its right operand. Looking at
the make just right function, if the
left operand of the catch evaluates into
an error set value, that value is
captured as variable ERR in the right
operand, and then this error is returned
by the catch.
In the fix too big and fix too small
functions, again, errors are captured by
the catch operations, but in these
cases, the right operands are block
statements denoted by curly braces.
Be clear that any return statement in a
block returns from the whole function,
not just the block.
Though in a later exercise, we'll learn
a way to return a value from just the
scope of a block.
In exercise 25, we see a try operation
which is simply a shorthand for a catch
operation that captures and returns the
error.
In the add five function, if the call to
detect returns an error, the try
immediately returns that error.
This is the same as if we used a catch
operation that captured the error from
the detect call and used the return
statement to return the error.
In exercise 27, a defer statement is
used to defer evaluation of an
expression.
Here the first call to print is
deferred, meaning that it won't execute
until execution leaves this scope.
In this case, the defer is in the
top-level scope of the function, so when
execution leaves the function, the
deferred print will be executed.
Effectively here, the string apple is
printed before the string banana.
In exercise 29 an error defer statement
is used, which defers an expression, but
the expression is only evaluated if an
error is being returned.
In the make number function here, a
print statement is error deferred, so it
only executes if an error is returned
from the function.
In exercise 30, we use a switch
statement to switch on the value of a
U8.
Unlike C, a switch case does not fall
through to the next, so we don't put a
break statement in each case.
The default case is denoted by the
reserved word else.
Exercise 31 demonstrates a switch used
as an expression.
The switch evaluates into the expression
of the executed case.
In exercise 32, an unreachable statement
is used to denote a code path that
should never execute. If executed,
unreachable triggers a panic, meaning an
unrecoverable termination of the
program.
Unreachable statements can be useful in
development to help guard against
unintended code paths.
In exercise 33, an if else statement is
used to branch on an error union value.
If the value is an error, the else
branch captures the error and executes.
Otherwise, the if branch captures the
payload value and executes.
In exercise 35, an enum type is defined
and assigned to constant ops.
This enum has three named values, inc,
pow, and dec.
In the main function here, a switch is
used to branch on the three enum values.
In exercise 36, the enum type color is
backed by integer type U32, meaning each
value of the enum has an associated U32
value.
This allows the enum values to be cast
to integers with the built-in int from
enum function.
In exercise 37, we see the definition of
an example struct type, which is
assigned to constant character.
This struct is composed of four fields,
a field named role of type role, which
is defined above as an enum,
a field named gold of type U32,
a field named experience of type U32,
and a field named health of type U8.
Inside the main function, we initialize
a variable with a literal of this
character struct type, and in the
literal, we give a value for each field.
Notice that each field name in the
literal is prefixed with a dot.
After creating the struct instance, we
access its fields with the dot operator,
just like in C or other similar
languages.
Next, exercise 39 demonstrates basic
usage of pointers.
The syntax is very similar to C, except
the asterisk for dereferencing is placed
in post position and separated by a dot.
First here, we create a pointer from
variable num1, which we assign to
constant num1 pointer,
and then we assign the dereference of
num1 pointer to variable num2.
Also note that we are allowed to assign
to the dereference of a pointer, even if
that pointer is stored in a constant.
Unlike a variable, a constant cannot be
assigned a new value after
initialization, but dereferencing
modifies the location referenced by the
pointer, rather than modify the pointer
itself.
Hence, this assignment is allowed.
Next, in exercise 40,
the variable P has the type pointer to a
constant U8, meaning it stores a pointer
to a U8 that does not allow assignment
to its dereference.
Attempting to assign to the dereference
of a pointer to a constant will trigger
a compilation error.
However, if the location pointed to by
the pointer is not itself constant, then
it is possible to modify the value
directly.
Here, P's last assignment is the address
of Y, and we subsequently modify the
value of Y.
When we then dereference P, we get the
last value assigned to Y.
In exercise 45, the function deep
thought returns a so-called optional
type as denoted by the question mark
prefix before U8.
This optional type encompasses all U8
values plus the special value null. So,
this function can return either any U8
value or null.
Be clear that unlike in other languages,
what Zig calls null isn't necessarily
related to pointers, as in this case
where we have the optional variant of
the U8 type.
As we'll see in the next exercise
though, pointer types can also be made
optional.
Anyway, in the main function here, deep
thought is called as the left operand of
the or else operator.
When the left operand of an or else
evaluates to null, then the or else
evaluates and returns its right operand.
Otherwise, or else just returns its left
operand.
So here, because deep thought always
returns null, this or else will return
42.
The next exercise 46 defines an elephant
struct, which contains a field named
tail with type optional pointer of
elephant.
This means that the tail field can be
assigned either an elephant pointer or
the value null.
The parameters of the function link
elephants are also optional pointers of
elephant.
Even if we're certain that an optional
pointer value is not null, we still must
use an or else operation to get the
plain pointer value.
For the right operands of the or else
operations here, we use unreachable, so
if the optional pointers actually are
null, these or else operations will
trigger panics.
Because this pattern is so common, Zig
provides a shorthand syntax, question
mark after a dot, as demonstrated by the
next line, which is equivalent to the
prior.
Lastly, notice in main that we use the
regular address operator for the
arguments to the link elephant function.
What's happening here is that the
address operator returns a regular
elephant pointer,
but in this context, the compiler will
coerce the regular pointer into an
optional pointer.
In exercise 47, functions are included
as members in structs.
In the alien struct, there is a member
function named hatch, and in the heat
ray struct, there's a member function
named zap.
Function members of a struct belong to
the namespace of the struct, but if
their first parameter is the enclosing
struct type, then they can be called
with traditional method call syntax.
In this example, the hatch function in
the alien struct doesn't take an alien
as its first parameter, so it can only
be called as alien.hatch.
The zap function in the heat ray struct
does have heat ray as its first
parameter type, so it can be called
either as heat ray.zap, or instead, we
can write the first argument, then dot
and zap, then in the parameter list
parentheses, we pass the remaining
arguments,
just like the syntax of a Java or C#
instance method call.
Either way, the result is the same.
In exercise 50, the first variable is
initialized with the special value
undefined.
This means the initial content of the
variable will be whatever happened to
reside at that variable's location in
memory before it was created.
The rest of the code demonstrates that
string literals can be cast to pointer
to constant arrays of U8s, as long as
the array size matches the number of
bytes required to store the string.
The variable first line one has a type
pointer to constant array of 16 U8s, so
it can be assigned a string literal with
16 bytes of character data.
Then the second variable has an error
union type where the payload is a
pointer to constant arrays of 21 U8s, so
it can be assigned a string literal with
21 bytes of character data.
In the next exercise 52, we see examples
of slices.
A slice in Zig contains a pointer and a
length, so it effectively represents a
sub range of an array.
Here the variable cards is an array of
eight U8s,
and then constants hand one and hand two
are slices of U8s.
The range notation inside the square
brackets indicates a slice operation,
where the first integer is the starting
index of the sub range, and the second
integer is the end index of the sub
range, so the length of the sub range is
the second integer minus the first.
When the second integer is omitted, it
defaults to the length of the array.
Hand one is assigned a slice
representing the sub range of the cards
starting at index zero with length four.
And hand two is assigned a slice
representing the sub range of the cards
starting at index four, also with length
four because it runs to the remaining
end of the cards array.
In exercise 53, a string is sliced.
Because the string type is a pointer to
a constant array of U8s, slicing a
string produces a slice of const U8s.
Note that the use of constant type
declarations seems a bit inconsistent.
There's no such thing in Zig as an array
of constants, nor is there such a thing
as a constant array, yet Zig does have
pointers to constant arrays.
For slices, the values of the slice
itself can be made constant, which
prohibits modifying the values through
the slice.
Like with arrays, you you cannot create
a slice which itself is a constant, but
you can create a pointer to a constant
slice.
Confusingly though, unlike with pointers
to constant arrays, you can modify the
elements via a pointer to a constant
slice.
What you can't do via a pointer to a
constant slice is modify the slice
itself, meaning its pointer and length.
This is very confusing, so let me
restate it.
First, you cannot create an array with a
constant element type, but you can
create slices with a constant element
type.
And second, you cannot create arrays or
slices which are themselves constant,
but you can create pointers to constant
arrays or slices.
The elements of a pointer to a constant
array cannot be modified, but the
elements of a pointer to a constant
slice can,
unless the elements themselves are
declared constant.
Anyway, const is definitely one of the
more confusing aspects of Zig, so don't
worry if it takes a while to get
straight.
The next exercise, 54, contains what Zig
calls a many item pointer,
which is a pointer that allows for
indexing and pointer arithmetic.
In this example, the local constant many
PTR is a many item pointer of const U8s.
The next line reads index five of the
pointer, meaning the U8 value that is
five U8s up in memory from the location
represented by the pointer, just like
adding five to a pointer in C.
In the line after, the many item pointer
is sliced from index zero up to index
S.len.
In exercise 55, a union type is defined
called insect. A union in Zig has
members like a struct, but unlike a
struct, the fields of a union overlap
each other in memory. So, effectively,
only one field of a union instance is
active at a time.
The insect union here in this example
has an ant struct field and a bee struct
field, and if you assign the one field,
you're effectively clobbering the value
of the other.
Because by default, unions in Zig are
not tagged, there's no way to tell from
the union itself which of its fields is
currently active. So, the code here
creates an enum species with values ant
and bee to track which of the insect
fields is active.
In the main function, instances of the
ant and bee structs are created, and
then an instance of insect is created
with its ant field initialized.
The insect is passed to the print insect
function along with the species.ant enum
value to indicate that this insect
represents an ant.
Then, another instance of insect is
created, this time with its bee field
initialized, and this instance is passed
to the print insect function along with
the species.bee enum value.
Again, without the enum, the print
insect function wouldn't know which
field of the insect to use.
In exercise 56, again an insect union is
defined, but this time the species enum
is stored as a tag in the union type
itself.
Now, when a field of an insect instance
is assigned, its tag is set accordingly.
Inside the print insect function then,
the code can switch directly on the
insect value itself.
Be clear though that the cases of the
switch still correspond to the values of
the species enum, not the fields of the
union directly.
In exercise 57, this time the insect
union stores an auto generated enum as a
tag, as indicated by the reserved word
enum in the parentheses.
The auto generated enum type has a value
corresponding to every field of the
union, so we don't need to create a
separately defined enum type.
In exercise 62, a for loop is used as an
expression.
In the loop, a break statement specifies
the value produced by the loop.
Because the compiler can't know if a
loop is guaranteed to break, a for loop
expression always requires an else
clause to guarantee that the loop
produces some value.
In this case, the loop looks for the
first string that has exactly three
bytes, but failing to find any match, it
will produce the value null.
After the loop, the following if
statement has an optional type for its
condition.
The else clause of this if will execute
if the value is null, otherwise the if
clause executes with the captured non
null value.
In exercise 63, an outer loop is given a
label, enabling any break and continue
statements inside any of its nested
loops to break or continue from the
outer loop.
Inside main here, the outer loop here is
given the label food loop, and then a
continue statement inside a nested loop
specifies this label to continue the
outer loop.
All other break and continue statements
here do not specify a label, so they
apply to the loops in which they're
directly contained.
Note that a label is defined with a
colon after the name, but then when used
in a break or continue statement, the
colon precedes the name.
In exercise 64, we see some calls to a
couple of Zig's built-in functions,
which are denoted by an at symbol
prefix.
First, the add with overflow function
returns the result of adding two
numbers, plus also a bit indicating
whether the addition triggered overflow.
Second, the bit reverse function
reverses the values of the bits in an
integer value.
Exercise 65 demonstrates a few more
important built-in functions.
The at type function returns the type
that it is called inside, in this case
the struct narcissus.
As you can imagine, this requires
special compiler support, hence why it
is a built-in function.
Inside the type to string function
defined here, the at type name function
is called.
The type name function takes a type as
argument and returns its name as a
string.
Inside main, the at type of function is
called. When called with one argument,
this returns the type of the argument,
but when called with multiple arguments,
it returns the best fit type which they
can all be coerced to.
Zig calls this peer type resolution.
In this case though, the arguments are
all of the same type narcissus, so that
is the type returned.
Next, the at type info function is
called. This function takes a type
argument and returns a struct with its
type information.
In this example, the code uses type info
to print the names of the narcissus
fields as strings.
Lastly here, note that the field structs
is written as a string with an at symbol
prefix.
This is special syntax for identifiers
that otherwise are reserved words in the
language.
Because struct is a reserved word in
Zig, we need this special syntax to
access the type info field named struct.
Exercise 66 demonstrates that number
literals are comptime types.
An integer literal is a comptime int,
and a float literal is a comptime float.
Because these types are expected to
exist only at compilation time, we can
create constants to store these types,
but not variables.
However, these types can be coerced into
the other numeric types, such as in the
following line, where an integer literal
is coerced into a U32, and a float
literal is coerced into an F32.
In exercise 67, a comptime variable is
created, meaning a variable which can be
modified at compile time, but which
functions as a constant at runtime.
We need to back up a second though and
talk about how to think about Zig's
compile time execution.
The simplest way to think of it is that
as the compiler processes each
statement, it asks two questions. One,
should the statement be executed right
now during compilation? In other words,
is it a comptime statement? And two,
should the statement be part of the
generated code? In other words, is it a
runtime statement?
Some statements are either just comptime
or runtime, but some statements are
both.
You may wonder though how code can be
executed during compilation if it hasn't
yet been compiled already.
Well, in short, the compile time code is
interpreted. The compiler translates a
comptime statement directly into action,
rather than generated machine code. Or
in other words, the compiler reads the
statement and does what it says to do.
Now, the details of course get more
complicated, but this is generally an
accurate enough mental model for a user
of the language.
So anyway, in this example, we have a
comptime variable count, which is
initialized with the value zero.
Because the variable is comptime, any
assignment to the variable is also
implicitly executed at compile time.
However, the value of a comptime
variable can still be used in runtime
expressions, in which cases it acts like
a constant that has whatever value was
last assigned to it.
In this example, though, count is only
used in expressions that can be fully
evaluated at compile time.
After each time the count is
incremented, it is used with a double
asterisk operator and a struct literal
containing the character A.
The end result is that constant A1 is a
struct with a single character A,
constant A2 is a struct with two
characters B,
constant A3 is a struct with three
characters C, and constant A4 is a
struct with four characters D.
In exercise 68, the function scale me
inside the struct schooner has a compile
time parameter.
This means the argument passed to this
parameter must be a constant or compile
time value, and the function is
separately compiled for each unique
combination of arguments to its compile
time parameters.
The scale me function in this example is
called with three different values for
its compile time parameter scale,
so it is compiled three times.
In exercise 69, the function make
sequence has two compile time
parameters, one for a type and one for a
size.
The function then returns an array of
this type and size.
Inside the function, it initializes the
array it will return with increasing
integer values.
This requires using the built-in
function int cast, which casts a value
into the target type expected from
context, and also the built-in function
as, which casts a value into a target
type.
Because all parameters of this function
are compile time, and the function does
not depend upon any runtime globals,
calls to the function can execute fully
at compile time.
Also note that because the function
casts an integer to the type parameter
T, T must be an integer type.
Any call to make sequence that tries to
pass a non-integer type would trigger a
compilation error.
In exercise 70, the last function is a
duck has a parameter of type any type,
meaning the compiler accepts any kind of
value passed to this parameter.
A function with any type parameters is
separately compiled for each unique
combination of types passed to its any
type parameters.
Inside this function, built-in functions
are used to get information about the
parameter's type.
Type of returns the type and has decl
returns true if the passed type has a
member with a name matching the passed
string.
So here, walks like duck will be true if
the argument has a member named waddle,
and quacks like duck will be true if the
argument has a member named quack.
Note that any if with no runtime
expressions in its condition will be
evaluated at compile time, and if the
condition is false, then the body of the
if is omitted from the generated code
entirely.
So here, when is a duck is called with
an argument not having both waddle and
quack members, the call will not invoke
the quack method of the possible duck,
and in fact the generated code will
contain no such branch at all.
In exercise 71, a for loop has the
modifier inline, which means the loop is
unrolled at compile time.
An unrolled loop is iterated at compile
time and generates runtime code for each
iteration.
Here, the loop iterates over the compile
time value fields, so the loop is
allowed to be inlined.
Inside the loop, the if condition is
evaluable at compile time, so the body
of the if is only included in the
generated code of each iteration when
its condition is true at compile time.
Be clear, though, that again, this is
generally true of if statements, not
just if statements inside inline loops.
In exercise 72, this time a while loop
has the modifier inline.
Again, the loop is iterated at compile
time and code is generated for each
iteration.
This is allowed here because both the
condition and the post condition of the
loop are evaluable at compile time.
Inside this loop, the switch is also
evaluable at compile time, so each
iteration contains generated code for
just the single matching case.
Like with ifs, this is generally true of
all switch statements, not just switch
statements inside inline loops.
In exercise 73, a compile time statement
is used to make an expression evaluable
at compile time.
The function get llama takes a single
compile time parameter, but the function
reads from a global non-constant array,
so calls to the function must still
execute at runtime.
However, the call to assert in the
function can be made to run at compile
time with the compile time reserved
word.
This way, calls to get llama with an
index out of bounds will trigger a
compilation error rather than a runtime
error.
Lastly about compile time, exercise 74
demonstrates that code in the file
scope, meaning outside of any function,
is always implicitly executed at compile
time.
The global constant here, llamas, is
initialized with a call to make llamas,
which takes a compile time argument.
Though the expression is not explicitly
marked as compile time, it is compile
time implicitly.
If we do try to add the compile time
reserved word here, the compiler will
complain that it's redundant.
Exercise 76 introduces
sentinel-terminated arrays.
A sentinel is a special designated value
that signals the end of an array.
C strings, for example, use a zero byte
to denote the end of the string.
Here, the variable nums stores a
sentinel-terminated array with sentinel
value zero, as indicated by the colon
and zero inside the square brackets.
As usual, the underscore indicates that
the length of this array is inferred
from the number of values in the
literal, in this case six.
Because the sentinel itself must be
stored at the end of the array, though,
this array requires storage for seven
contiguous U32s in memory rather than
just six.
Because the end of the array is supposed
to be indicated by the sentinel value
zero, the array generally should not
contain zero as a normal value. However,
Zig does not enforce this restriction.
Many item pointers can also be
sentinel-terminated, such as this
constant PTR, which is a zero-terminated
many item pointer of U32s.
Concretely, it is still just a pointer,
but when indexed, the value zero is
expected to indicate the end of the
data.
Lastly here, the function print sequence
uses compile time introspection to print
information about these types.
In the case of a pointer, the function
calls the built-in sentinel to get the
sentinel value of the type, which in
this case is zero.
Exercise 77 demonstrates that the true
type of a Zig string literal is a
pointer to a constant zero-terminated
array of U8s.
Such a pointer can be cast or
automatically coerced to several
different other types, in this case a
many item pointer of const U8s.
In exercise 78, the built-in function
pointer cast is used to cast a
multi-pointer of const U8s into a
zero-terminated many item pointer of
const U8s.
Like a few other built-ins we've seen,
this built-in infers its return type
from the calling context.
In exercise 80, the function circle
returns a new type.
Because types only exist at compile
time, that right there tells you that
this function can only execute at
compile time.
The function takes a compile time type
parameter, and this type parameter is
used in a struct definition that is
directly returned from the function.
So the function returns a struct type
where the fields have the type of the
type argument.
In the main function here, the circle
function is first called with argument
I32,
so it returns the struct type where T is
I32, and then this type is used in a
struct literal that is assigned to the
local constant circle one.
Then the circle function is called
again, but this time with argument F32,
so it returns the struct type where T is
F32, and again this type is used in a
struct literal that is assigned to a
local constant.
In exercise 81, two anonymous struct
literals have three fields of the same
name but different types.
These structs are then passed to the
print circle function, which takes an
any type parameter, and the function
prints out the values of these fields.
Because the print circle function
doesn't depend upon the types of the
fields, print circle can be compiled for
both of these anonymous structs.
Also, the fact that one struct has an
extra field is irrelevant because print
circle simply ignores it.
In exercise 82, an anonymous struct is
created with values that have no field
name.
These values are implicitly assigned to
numbered fields starting from zero.
So here, the value true is assigned to
field zero, the value false is assigned
to field one, the I32 value 42 is
assigned to field two,
and the F32 value 3.141592
is assigned to field three.
In the print tuple function, compile
time introspection is then used to print
the names, types, and values of these
fields.
In exercise 83, we see a struct with
numbered fields coerced into an array.
The struct has five U8 values for fields
zero, one, two, three, and four,
and so the struct can be coerced into an
array of five U8s.
In
In exercise 92, three struct types, ant,
bee, and grasshopper, all have a member
function named print that takes a value
of their own type.
The union type insect has a member for
each of these structs and defines its
own print function that takes an insect.
Inside this function, a switch over the
insect value contains just the else case
marked with the reserved word inline,
which means that compile time a case is
generated for each member of insect.
So, here then we get three generated
cases, each of which invokes the print
method of its respective insect member
type.
This inline else convenience spares us
from manually writing out cases for each
insect member to invoke their respective
print methods.
So, in the main function here then,
insect values are passed to the insect
print method, which switches on the
member type and invokes the appropriate
struct's print method.
Exercise 93 demonstrates how to import C
code.
The built-in function C.import takes as
argument a block which is interpreted as
C code.
Inside the block, calling the special
built-in C.include will transpose the
content of the specified file just like
the include directive in C.
Note that C.import can only be called
inside the block passed to C.import.
The C.import function itself returns the
module of C code as a module type.
Then inside the main function here, the
imported C print function is invoked.
The function takes a C int, an optional
pointer of const anyopaque, and a C
uint, and the function returns a C uint.
In exercise 96, an arena allocator from
the standard library is used to manually
allocate memory.
First, an arena allocator is initialized
with its deinitialization deferred
immediately in the next line.
Then the allocator method is called to
get a wrapper type, which has a create
method that allocates memory of the
specified size.
In this case, memory is allocated for an
array of F64s.
This array is then passed to a function
which stores the running averages of
values from another array.
Note that we don't deallocate the
individual allocations from the arena.
The whole idea of an arena is that all
of its memory is deallocated together.
So, here the deferred deinit call on the
arena itself is what will deallocate
this memory.
In exercise 100, a for loop iterates
over two arrays in tandem.
This is allowed for arrays which have
the same length.
In exercise 101, a for loop iterates
over three arrays and a range.
Again, this is allowed as long as all
the sequences have the same length.
This range starts at one and leaves its
end unspecified, so its length will
automatically match the other sequences.
Exercise 102 demonstrates a few tests.
A test block is marked by the word test
and its name is specified as a string.
Tests are executed by running the zig
test subcommand,
and each test is effectively like a
function that returns an error or void.
The expect function of the testing
module returns an error if its argument
is not true. Expect equal returns an
error if its two arguments are not
equal,
and expect error returns an error if its
second argument does not match the error
passed as its first argument.
In exercise 104, the main thread spawns
a few additional threads and waits for
them to terminate.
To create a thread, we call
std.thread.spawn.
The first argument is an anonymous
struct of config options, which when
empty means we're using all the
defaults.
The second argument is the function to
invoke in the thread,
and the third argument is an anonymous
struct of values to be passed to the
function.
In this case, the function being called
in the threads takes a single integer,
and the values one, two, and three are
being passed to each of the three
threads, respectively.
The calls to spawn return handles that
represent the threads, and calls to join
on these thread handles are immediately
deferred after spawning.
When we call join in the main thread,
the main thread will spin and wait for
the join thread to finish execution if
it hasn't finished execution by that
point already.
Thus, after join returns, the join
thread is known to have finished
execution.
Because all these defers are inside a
code block, the join calls will run when
execution leaves that code block. So,
the print statement that is after the
block will not run until after the
threads have all finished running.
But before leaving the block, the main
thread also gets an instance of
std.io.threaded
and calls its sleep function to make the
main thread sleep for a minimum of 5
seconds.
Note that the expression
.initSingleThreaded
is shorthand for
std.io.threaded.initSingleThreaded,
and also the expression .await is
shorthand for std.io.clock.await.
These namespaces can be left implicit in
these cases because they can be inferred
from the context.
In exercise 108, a switch is given a
label so the code in the switch can
break out of or continue the switch.
To continue a switch means to jump back
to the start of the switch with a
different value.
For example here, the approved case
continues with the merged value, so
execution effectively jumps from the
approved case to the merged case.