Regarding 1. If you write a function that reaches the end without returning (when it isn’t void) zig complains at you, like this:
fn doSomething(x: i32) bool {
if (x == 5) return true;
}
pub fn main() !void {
_ = doSomething(5);
}
Output:
patternsquestion.zig:3:24: error: function with non-void return type 'bool' implicitly returns
fn doSomething(x: i32) bool {
^~~~
patternsquestion.zig:5:1: note: control flow reaches end of body here
}
^
I think you shouldn’t pre-emptively add @panic
or unreachable
at the bottom, because those signal to the compiler “I want a runtime crash if something gets here” (instead of the compile time error which is better) and “I made sure this can’t happen” (which you didn’t if you are unsure) respectively, so in both of these cases it is better to get the compiler error that the control flow reaches the end of the body.
And when you get that error, then you can think about what the right thing to do is,
whether you know enough, to make that decision, whether using @panic
or unreachable
is appropriate.
There are also cases, where you can just change the code to avoid needing @panic
or unreachable
. Instead of:
const Option = enum { A, B };
fn choose(o: Option) u32 {
switch (o) {
.A => return 4,
.B => return 764,
}
unreachable;
}
Write this:
fn chooseBetter(o: Option) u32 {
return switch (o) {
.A => 4,
.B => 764,
};
}
Instead of only thinking about each case individually (hoping the unreachable is actually correct):
sidenote: this probably still would be better as a switch expression, imagine a situation where a switch doesn’t work well
const Case = enum { A, B, C };
fn handleCases(c: Case, val: i32) i32 {
if (c == .A and val == 15) return 25;
if (c == .A) return 0;
if (c == .B and val > 3) return 9;
if (c == .B) return 0;
if (c == .C and val < 0) return 100;
if (c == .C) return 0;
unreachable;
}
Think if there is some default case that always applies, when none of the early exit conditions apply. To get there sometimes it is helpful to reverse the way you are thinking about the problem. In this case maybe you could try writing the function in reverse starting from the last return statement (that is unconditional). Also tangentially related: think about whether your problem is a dynamic programming problem that has a certain kind of base case.
fn handleCasesBetter(c: Case, val: i32) i32 {
if (c == .A and val == 15) return 25;
if (c == .B and val > 3) return 9;
if (c == .C and val < 0) return 100;
return 0;
}
I think you should consider all the things above before you consider using @panic
or unreachable
at the end of the function.
And if you decide to use them, it is probably still a good idea to use plenty of std.debug.assert
s and writing lots of test cases, to make sure that you don’t have a logic error or faulty assumption somewhere.
For functions with a small number of combinations, you can exhaustively check them, for other functions, you can use fuzzing and try to isolate the function within a module, so that it is used by code in one particular manner that is well tested.
Regarding 2. I think it is ok, personally I tend to put these checks in local named functions and then write:
fn eggs(ptr_tgt: anytype) void // Expect a pointer to a mutable struct
{
comptime checkMutableStructPointer(@TypeOf(ptr_tgt));
...
}
But that depends a bit whether the check is complex, used multiple times and what you find more readable.