Return from recursive loops using ?void

A nice feature seems to be that a function returning ?void is not required to return something.

like this:

fn do_something() ?void
{
    std.debug.print("do {}\n", .{x});
}

or…

fn do_something(x: usize) ?void
{
    if (x > 42) return null;
    std.debug.print("do {}\n", .{x});
}

I need a (fast) mechanism to return from a recursive loop.
Would it be a good idea to use the ?void trick like this?

fn generate() void
{  
    while (true)
    {
          do_something() orelse return; // using the orelse to escape from recursion
    }
}

Currently I have a quit: bool field in my struct and I check this field once in a while here and there, which is quite inconvenient.

Congratulations, you have reinvented booleans.

fn generate() void
{  
    while (true)
    {
          if(do_something()) return;
    }
}

Not sure why you jumped from a quit: bool field to returning ?void, when you could also just return a bool.

I would argue that returning a bool is better, as it indicates a logical condition, as opposed to optionals which indicate the presence or absence of a thing, in this case the presence or absence of nothing lol.

1 Like

By the way the same trick also works with errors.
Either way I would say go with the type that best describes what you want to do, and a ?void is kind of the least descriptive option. An error{BreakRecursion}!void would be better in my opinion, since you can at least give it a name while having similar semantics.

3 Likes

The thing is: during recursion I do not want to return values. That is why I reinvented the boolean :slight_smile:

There are some other conditions inside the recursion where I simply want to exit the function.
In these cases I would have to return some meaningless value.

Errors work the same way yes… But that would feel even more weird. It is not an error.

I agree it is weird and maybe not incredibly clear, but quite convenient.

const STOP: ?void = null;

fn gen(...) ?void
{
    while (true)
    {
        if (not_found) return; // meaningless value
        go_on(...) orelse return STOP;
    }
}

fn go_on(...) ?void
{
    if (no_more) return;  // meaningless value
    store(...) orelse return STOP;
    gen(...) orelse return STOP;
}

fn store(...) ?void
{
    // store something.
    if (some_condition) return STOP;
    // no return value needed
}
1 Like

I would try to use a labeled switch with continue instead, instead of all the recursion and separate functions.

4 Likes

Impossible that is. The recursive functions contain comptime parameters. I would have to use a stack etc. In reality the code is much more complex depending heavily on the comptimes and 5 other parameters.

(edit: although I would like very much to have it non-recursive. did not dare to try it yet!)

2 Likes

If you’re using the return value of {}/null for control flow at the call site, it doesn’t seem like the return value is meaningless :thinking:. I think it’s just a matter of encoding this meaning as a bool instead of as a ?void. Still an interesting trick though!

I kinda like it tbh, because it’s clearer (to me at least) that the null return value is the exception and means “stop”, while with a boolean it’s unclear whether the ‘exceptional’ return value is true or false (e.g. does return value true mean “please continue” or “please stop”).

2 Likes

I don’t think it is impossible, whatever you have implemented the comptime parameters don’t exist at run time, so it should be possible to transform the code, but depending on how complex your code is, it might be difficult.

Still transforming the code might be worth it and could even make it more clear (not necessarily, but possible) what is going on. You also could have a hybrid, where some parts work via recursion on some via labeled switch.

I would personally always favor writing code in whichever way induces the least cognitive load on the reader, but out of all the alternatives I think this suggestion hits the sweet spot between “convenient and concise to write” and “easy to understand”:

fn gen(...) error{Aborted}!void
{
    while (true)
    {
        if (not_found) return;
        try go_on(...);
    }
}

fn go_on(...) error{Aborted}!void
{
    if (no_more) return;
    try store(...);
    try gen(...);
}

fn store(...) error{Aborted}!void
{
    // store something.
    if (some_condition) return error.Aborted;
    // no return value needed
}

I will surely give it a try somewhere. Also I am curious about the performance in an iterative version.

The classic way to clarify the meaning of a bool is to use an enum with two values. This works just fine in Zig.

For me, choice of bool versus null is equally arbitrary and uninformative.

1 Like

I like this actually. The call site can’t ignore the null arm, so it’s about the caller.

I’m interpreting your example as returning null to mean “no side effect occurred”, and void aka not returning to mean “did side effects”. Which is kind of elegant, in my opinion.

The one thing here is that ?void is a confusing signature to encounter, without an explanation of why it’s being used. But that just means adding a comment about what’s happening.

Exactly. And inside the functions we can just return {nothing void}, which is very convenient in my complicated case. I just implemented it and works like a charm.
Long live the new boolean.

1 Like