Switch
Switch is a control structure that does two things: it allows executing a block of code and it can also return a value.
A switch is made up of branches (cases), each with its associated block of code. This way, each case defines what code to run if the variable has a certain value.
switch (variable){
value1 => {
// this code runs only if the variable equals value1
},
value2 => {
// runs only if the variable equals value2
},
…
valueA, valueB, valueC => {
// runs if the variable matches any of the values: valueA, valueB, …
},
…
valueN => {
// runs only if the variable equals valueN
},
…
Range of values => {
// runs if the variable falls within the specified range
},
…
else {
// runs only if the variable doesn't match any of the previous cases
}
}
Switch must cover all possible values of the data type being evaluated.
If you don’t explicitly define all cases or at least include an else branch, Zig will raise an error:
switch_incomplete.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age = 16; switch (n_age) { 0...3 => { print("You are a baby\n", .{}); }, 4...10 => { print("You’re a toddler\n", .{}); }, 11...15 => { print("You’re very young\n", .{}); }, 16...17 => { print("Young, but old enough to drive\n", .{}); }, } } |
|
$ zig run switch_incomplete.zig |
|
switch_incomplete.zig:6:5: error: switch must handle all possibilities switch (n_age) { ^~~~~~ |
In the switch above, we used value ranges defined with start_value...end_value. In this case, we have two possible solutions:
- define a type for n_age and make sure the range covers the full span of that type,
- or add an else block to cover all possible values.
switch_incomplete_fix_1.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age: u8 = 16; switch (n_age) { 0...3 => { print("You are a baby\n", .{}); }, 4...10 => { print("You’re a toddler\n", .{}); }, 11...15 => { print("You’re very young\n", .{}); }, 16...17 => { print("Young, but old enough to drive\n", .{}); }, 18...255 => { print("Yo’re an adult", .{}); }, } } |
|
$ zig run switch_incomplete_fix_1.zig |
|
Young, but old enough to drive |
In the solution, we defined the type of n_age as u8 (8-bit positive integers), because if we don’t specify a type, Zig will infer it (remember from Chapter 1) as comptime_int. That type is like saying “integers with infinite range.”
Well infinite is just a way of speaking, no such thing exists on any machine, no matter how powerful. But since we didn’t tell Zig what type we’re going to use, it gives us a bit of leeway and “assumes” the value could be infinitely large, even though that’s not actually possible. And of course, you can’t define an upper range for a number that’s infinitely big.
Therefore, in this and many other cases, it’s better to define types explicitly, just like we did. That allows us to narrow down the case and fix our switch.
We can also add a simple else:
switch_incomplete_fix_2.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age = 16; switch (n_age) { 0...3 => { print("You are a baby\n", .{}); }, 4...10 => { print("You’re a toddler\n", .{}); }, 11...15 => { print("You’re very young\n", .{}); }, 16...17 => { print("Young, but old enough to drive\n", .{}); }, else => { print("You’re an adult", .{}); }, } } |
|
$ zig run switch_incomplete_fix_2.zig |
|
Young, but old enough to drive |
Returning values with switch
If we want to assign a value from a switch rather than just run code for a specific case, Zig allows that. This is useful when we want to store the value and use it later in the rest of the program.
const (or var) identifier = switch (variable){
value1 => return a value from here when the variable equals value1,
value2 => return a value from here when the variable equals value2,
…
valueN => return a value from here when the variable equals valueN,
…
Range of values => return a value from here when the variable falls within the range,
…
else => return a value from here if the variable doesn’t match any of the previous cases
}
Just like before, switch must cover all possible cases for the variable, and all branches must return the same type of value so it can be correctly assigned to the identifier.
In the following example, we want to check if a website exists. After accessing the address via HTTP, we received a response code and stored it in the constant n_code. Using switch, we translate the received code into a descriptive message, which we then print:
switch_http.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_code = 404; const s_response = switch (n_code) { 200 => "Page found", 301 => "Redirect", 404 => "Page not found", 500 => "Server error", else => "Unknown error", }; print("The address responded with: {d} '{s}'.\n", .{ n_code, s_response }); } |
|
$ zig run switch_http.zig |
|
The address responded with: 404 'Page not found'. |
Capturing the value in a branch
If we want to retrieve the value (payload) that was matched in a switch branch, we use the following syntax:
switch (variable){
value1 => |val1| val1 is captured when the variable equals value1 ,
value2 => |val2| val2 is captured when the variable equals value2 ,
…
valA, valB, valC => |val| val is captured if it matches any of these values,
…
valueN => |valN| captured when the variable equals valueN,
…
Range of values => |range val| captured when the variable falls within the range
,
…
else => |unknown value| captured when the variable doesn’t match any of the previous cases
}
Let’s look at an example to understand how this technique can be used. Imagine we want to print the HTTP response code along with a description:
switch_http_capture.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_code = 404; const t_resp = switch (n_code) { 200...399 => |n_c| .{ n_c, "Page exists or is being redirected" }, 400...499 => |n_c| .{ n_c, "Page not found or invalid request" }, 500...599 => |n_c| .{ n_c, "Server error" }, else => |n_c| .{ n_c, "Unknown error" }, }; print("The address responded with: {d} - '{s}'.\n", t_resp); } |
|
$ zig run switch_http_capture.zig |
|
The address responded with: 404 - 'Page not found or invalid request'. |
In the previous example, we return a tuple t_resp composed of the captured code number and a string. Functionally, that example doesn’t make much sense, since we already know n_code in advance. What matters in that code is understanding how to capture the value inside a branch when using ranges. Now let’s look at a more interesting version, where the captured value is actually used for something useful.
Switch with break and continue
switch_http_capture_2.zig |
|
const std = @import("std"); const print = std.debug.print; // simulate a request to a web page and get the response code fn get_dummy_page_response() u16 { return 404; } // simulate logging that a page does not exist fn dummy_db_pre_hook() void { print("Log in the database that the page DOES NOT EXIST\n", .{}); } pub fn main() void { // simulate getting the response code from a url const t_resp = sw: switch (get_dummy_page_response()) { // response 0 is impossible // but we use it here for an internal operation 0 => { dummy_db_pre_hook(); break :sw .{ 404, "Page not found" }; }, 100, 101, 102 => |n_c| .{ n_c, "That code does not exist" }, 200...399 => |n_c| blk: { break :blk if (n_c == 200) .{ n_c, "Page exists" } else .{ n_c, "Exists but redirected" }; }, 400...499 => |n_c| blk: { break :blk switch (n_c) { 400 => .{ n_c, "Bad request" }, 401 => .{ n_c, "Unauthorized" }, 403 => .{ n_c, "Forbidden" }, 404 => { // return to the outer switch passing value 0 continue :sw 0; }, else => .{ n_c, "Client error" }, }; }, 500...599 => |n_c| .{ n_c, "Internal server error" }, else => |n_c| .{ n_c, "Unknown response code" }, }; print("The address responded with: {d} - '{s}'.\n", t_resp); } |
|
$ zig run switch_http_capture_2.zig |
|
Log in the database that the page DOES NOT EXIST The address responded with: 404 - 'Page not found'. |
In this example, the response code is no longer defined as a constant - instead, we simulate a request by calling a function. This way, we're evaluating an expression directly inside the switch.
Capturing the value with |n_c| is necessary when using ranges, so we can work right after with the specific value that triggered that branch.
Also, thanks to the labeled blocks we saw earlier, we were able to return values using additional switch and if expressions. This gives us much more precision: for example, knowing exactly what kind of error we're dealing with in the 400 to 404 range. That already gives us much more control over what we want to return.
You may have noticed that the label is called blk: in both blocks. This is very common in Zig. Labels don’t have any special meaning beyond what you want them to represent. As we mentioned earlier, if labels aren't nested, you're not required to give them different names. The name blk: that you'll often see in Zig examples doesn’t have any special meaning (probably short for "block"). It’s just an informal convention.
We also labeled the switch with the label sw: (it could have any other name you choose to give it).
Why do we do this?
It’s a powerful feature: it allows us to jump back to the start of the switch after entering one of its branches, and even pass it a new value to evaluate:
|
404 => { // we go back to the switch, passing the value 0 continue :sw 0; }, |
This causes the switch to be re-evaluated, but this time with the value 0. In our case, the code for case 0 does the following:
|
0 => { // write to the database dummy_db_pre_hook(); break :sw .{ 404, "Page not found" }; }, |
Yes, in this case we could have called dummy_db_pre_hook() directly in the 404 case. The interesting part of the example is showing how we can jump from one branch to a different one.