Loop-once/do construct

A “loop” of exactly one iteration might seem completely useless, but it’s actually helpful in certain situations. Consider the following code:

    inline for (.{{}}) |_| {
        if (!conditionA) break;
        if (!conditionB) break;
        if (!conditionC) break;
        if (!conditionD) break;
    } else {
        // all conditions met
    }

That’s basically a short-circuited if statement:

if (conditionA and conditionB and conditionC and conditionD) {
   // all conditions met
}

The loop construct allows us to keep the conditions on separate lines and also allows for side-effects, with std.debug.print() being the most obvious.

So I was thinking, a shorthand for inline for (.{{}}) |_| { could be quite useful. As do already exists in C that seems the best candidate:

    do {
        if (!conditionA) break;
        if (!conditionB) break;
        if (!conditionC) break;
        if (!conditionD) break;
    } else {
        // all conditions met
    }

The same construct could be used in situations where we need to use a block currently. For example, constant/variable initialization :

    const array = do {
        var a: [10]usize = undefined;
        for (0..10) |i| a[i] = i;
        break a;
    };

Eliminates the need for a more-or-less superfluous label. The ability to add an else clause also means extra flexibility.

So do would be interpreted as inline for (.{{}}) |_|. Not a major change to the language. Just a shorthand.

Why not…?

    do: {
        if (!a) break :do;
        if (!b) break :do;
        if (!c) break :do;

        // all conditions are met.
    }
13 Likes

Yeah, I use that idiom myself, but I call the label once:

    once: {
        if (!a) break :once;
        if (!b) break :once;
        if (!c) break :once;

        // all conditions are met.
    }
4 Likes

Because I consider the usage of break :label harmful. It’s basically goto by a different name.

goto itself isn’t harmful. The original “goto considered harmful” article talks about global goto (setjmp, longjmp in C for example), but many people did not get that and label anything resembling goto as a bad thing. Indeed, continue, break, while, for they are all equivalent to local goto (which is what C’s goto is).

13 Likes

Unrestricted goto can become very hard to follow – I’ll give you that. But a break that exits from a lexical scope is the perfect tool to deal with early termination. I think it pairs extremely well with defer, and I love both.

6 Likes

That’s why I’m proposing what I’m proposing. Using break to bypass certain code path is easier than relying on short-circuiting of logical statements.

Here’s some actual code where I do a loop-once:

    .@"fn" => |f| {
        td.attrs.is_supported = inline for (f.params) |param| {
            if (param.is_generic) break false;
            if (param.type == null) break false;
        } else inline for (.{1}) |_| {
            if (f.is_generic) break false;
            const RT = f.return_type orelse break false;
            const retval_attrs = self.getAttributes(RT);
            if (retval_attrs.is_comptime_only) break false;
        } else true;
    },

It checks whether a function is supported by checking its parameters and return value. I’m using for(.{1}) |_| here so that break statements in it would resemble those in the first for (). The availability of an else clause also means that we only break to false.

Would this code achieve the same thing?

    .@"fn" => |f| {
        td.attrs.is_supported = supported: {

            inline for (f.params) |param| {
                if (param.is_generic) break :supported false;
                if (param.type == null) break :supported false;
                if (f.is_generic) break :supported false;
            }
            if (f.is_generic) break :supported false;
            const RT = f.return_type orelse break :supported false;
            const retval_attrs = self.getAttributes(RT);
            if (retval_attrs.is_comptime_only) break :supported false;

            break :supported true;
        }
    },

Personally, I would find this variant easier to understand.

1 Like

People might want to read the original letter from Dijkstra because this comment has nothing to do with what it says and completely misrepresents it. Mention of setjmp/longjmp, which is not in any meaningful sense a “global goto” (it’s a stack unwinding operation) is utterly anachronistic–such a thing didn’t exist at the time and is not what Dijkstra was talking about.

I do however agree with the point that Cloudef was getting at: the construct here is not “basically goto by a different name” (that’s a failure to understand the concept of abstraction) and is not harmful … another reason to read Dijkstra’s letter to understand what he did consider harmful. Structured programming, which blocks containing break :label are an example of, was the cure. The problem was with unstructured gotos (very much “local”), which were rife at the time, not with control structures that are necessarily implemented using jump instructions.

It’s helpful to remember that Dijkstra said “It is practically impossible to teach good programming to students that have had a prior exposure to BASIC: as potential programmers they are mentally mutilated beyond hope of regeneration.” … it was the complete lack of structured control flow in languages like BASIC and FORTRAN II that Dijkstra was railing against, not structured control flow as seen in the loop-once construct being discussed here.

7 Likes

P.S. Here is a collection of Dijkstra’s pithy quotes: Edsger W. Dijkstra Quotes (Author of A Discipline of Programming)

All control flow could be considered an abstraction over goto. Exiting out of a block where the destination of the block is clearly marked by a block structure to me seems about as similar to goto as a return statement.

5 Likes