31
$\begingroup$

Anyone who's used C-family languages enough will have seen or used a "loop-and-a-half", which can be written in one of two ways:

foo();
while (condition) {
  bar();
  foo();
}

while (1) {
  foo();
  if (!condition) break;
  bar();
}

These examples show the basic structure of the "loop-and-a-half"; foo(); and bar(); here are placeholders for any statements. Often, the "foo" part reads some input, the "condition" checks that the input hasn't ended, and the "bar" part uses the input data somehow.

The former spelling involves code duplication, and the latter is error-prone (it's possible to forget the break statement and cause an infinite loop). It's possible to avoid both problems with a goto:

goto middle_of_loop;
while (condition) {
  bar();
middle_of_loop:
  foo();
}

but I've never seen this in practice, and newer languages tend not to have goto in the first place. Besides, goto has its own problems.

How could a C-family language make these easier to read and write? Does any language have a dedicated syntax for this?

$\endgroup$
2
  • 19
    $\begingroup$ I've never really considered the second version to be error-prone. It concisely expresses the logic, and forgetting the break is simply a logic error. $\endgroup$
    – Barmar
    Jun 30, 2023 at 23:27
  • $\begingroup$ Related: langdev.stackexchange.com/questions/95 $\endgroup$ Jul 23, 2023 at 9:50

16 Answers 16

22
$\begingroup$

Rust can achieve this without break or code duplication, since blocks are expressions:

while {
    foo();
    condition
} {
    bar();
}

Unfortunately this is a bit of an idiom, because the keyword while is separated from the actual condition for the loop; and the condition's location is marked by the } { line after it, which is weird. Perhaps if people get more used to the loop-and-a-half being written this way, it will become more intuitive.

Nonetheless it gets the job done, and the only language construct you need is block-expressions, i.e. a block delimited by braces which contains any number of statements followed by one expression, and the block's value is the value of the final expression.

$\endgroup$
7
  • 19
    $\begingroup$ sh, of all languages, also has this, just with a different spelling: while foo; condition; do bar; done. $\endgroup$
    – HTNW
    Jul 1, 2023 at 7:11
  • 2
    $\begingroup$ it splits the loop block into two which could be p a problem if you want to share a block scoped variable between the two parts. $\endgroup$
    – Jasen
    Jul 2, 2023 at 3:31
  • 1
    $\begingroup$ @Jasen That is a good point, but at least in Rust you can declare the variable outside of the loop, assign to it in the "foo" part, and Rust will know that it's definitely initialised when you use it in the "bar" part. $\endgroup$
    – kaya3
    Jul 2, 2023 at 3:36
  • 1
    $\begingroup$ I disagree that this is a good solution for this idiom. Firstly, it's a little surprising even to experienced users of Rust. Secondly, almost every time I've seen someone try this (from beginners on the Discord to colleagues at work), it's ended up with issues relating to ownership or aliasing, eg. foo() needs to own something that bar() needs later. And then you end up stuffing the output into an Option on the stack outside the loop, and presto, you've reinvented the Option based iterators but with less clarity and language support. $\endgroup$
    – detly
    Jul 2, 2023 at 5:57
  • 1
    $\begingroup$ @detly I didn't say it is a good solution; I said it "gets the job done" but is "unfortunately a bit of an idiom". Your point about ownership and lifetimes is largely addressed by my previous comment, but also not relevant unless you are designing the loop syntax for a language which has a Rust-like borrow checker (i.e. a tiny minority of languages). $\endgroup$
    – kaya3
    Jul 2, 2023 at 13:30
17
$\begingroup$

In languages with a C-style comma operator, you can use

while (foo(), condition) {
    bar();
}
$\endgroup$
12
  • 12
    $\begingroup$ You’re not wrong, but as a matter of personal preference I think it’s ugly. Besides, this assumes that “foo()” is only a single expression, which isn’t always the case. $\endgroup$
    – Bbrk24
    Jun 30, 2023 at 23:59
  • 5
    $\begingroup$ @Bbrk24 No, it doesn't. You can have as many as you want with the comma operator. I agree it's not a particularly readable style though. $\endgroup$ Jul 1, 2023 at 7:29
  • 8
    $\begingroup$ @JackAidley They still need to be expressions and not, say, nested loops. $\endgroup$
    – Bbrk24
    Jul 1, 2023 at 12:44
  • 1
    $\begingroup$ This is pretty similar to the Rust approach described in kaya3's answer $\endgroup$
    – user
    Jul 1, 2023 at 20:20
  • 3
    $\begingroup$ @Bbrk24 Nothing prevents you from designing loops as expressions. Also GNU C's Statement Expressions. $\endgroup$
    – Longinus
    Jul 1, 2023 at 23:10
15
$\begingroup$

A frame challenge: the problem isn't really with the loop construct itself, but with ad-hoc bundling of the "half" loop with the condition check. This is a problem that should be solved elsewhere, mainly by the programmer but perhaps also by making (possibly anonymous) function syntax easier in the language.

Note that the problem disappears entirely, even in C, if the first half of the loop really is wrapped up in a foo function and that function returns the desired condition value; then we can simply write

while(foo()) bar();

In C (or C++), even if that doesn't hold up, we can exploit the comma operator - so, in this case:

while(foo(), condition) bar();

So really, problems occur because there isn't already a foo and there is some resistance to making it. My experience - across multiple programming languages and something like 35 years of writing code - has consistently been that the common case, by far, is that the foo logic has the explicit purpose of setting things up so that the loop condition can be evaluated, and that this process is coherent and nameable. As such, the resistance to creating a separate function usually just seems lazy to me. (Of course, there are language-specific idioms for one-off functions, like blocks and IIFEs (really popular in Javascript). But I think code clarity often suffers this way. Naming things is quite useful.)

Another thing that can help is to make it easier to work with generators and iterators. For example in Python, reading from a file produces an empty result if and only if we are at the end, which allows a clever idiom for reading a binary file in chunks.


As a footnote: even in the cases where we'd want to use the comma operator in C or C++, we might want to ask ourselves why the code is designed that way. In the linked answer, we suppose that we need the length of a string that is populated by an I/O function (granted this appears to be a C++ example on what was asked as a C question). Sure, it would be contrived (and inflexible) to have that function report whether the resulting string meets some specific condition, and perhaps awkward to make a wrapper. Even just having it report the length seems arbitrary (and makes it harder to give the function a good name).

But - it could simply return the string itself by reference, and then while(read_string(s), s.len() > 5) simply becomes while(read_string(s).len() > 5). Similarly in C: if we pass a pointer to a buffer, just return that pointer as well (or perhaps a more useful one inside the buffer). The standard library already works that way, after all. (Some languages, like Python, resist the idea of a function returning a value and also having side effects, for well-considered reasons. But then we might ask if we wouldn't be happier with a new object anyway. Incidentally, the new-since-3.8 walrus operator can enable some tricks comparable to what I showed with the comma operator.)

$\endgroup$
4
  • 5
    $\begingroup$ "Note that the problem disappears entirely, even in C, if the first half of the loop really is wrapped up in a foo function and that function returns the desired condition value;" ─ this isn't a practical solution to the problem, because usually the "foo" part of the loop-and-a-half declares some variable (e.g. let line = input.readLine();) which is used in the condition and also in the "bar" part. Factoring out the "foo" part into a separate function moves the variable line out of scope for the "bar" part. $\endgroup$
    – kaya3
    Jul 1, 2023 at 16:21
  • 3
    $\begingroup$ @kaya3-supportthestrike I could have sworn I originally planned to address that, but now I'm not really sure what to say about it. It definitely is a complication. $\endgroup$ Jul 1, 2023 at 16:23
  • $\begingroup$ @kaya3-supportthestrike One approach is to make a new object first so that let line = input.readLine(); while (line) { bar(line); } becomes while (reader.readLine()) { bar(reader.line); } $\endgroup$
    – krubo
    Jul 1, 2023 at 20:59
  • $\begingroup$ Isn't the instantiation of a variable to be used for a loop, but declared outside the scope of a loop some sort of anti-pattern, as @krubo implies? Isn't this entire question based upon anti-pattern semantics? cf. softwareengineering.stackexchange.com/questions/142144/… $\endgroup$
    – Konchog
    Jul 2, 2023 at 12:19
11
$\begingroup$

A least-change approach would be to make it a compiler error if you write a while (true) or for (;;) loop without a loop-exiting statement (break, return, or throw) somewhere inside it. This would solve the “forgetting the break statement” issue similarly to how C# dealt with C's error-prone fall-through between case blocks.

Perhaps some special syntax could be introduced for the case when you deliberately do want a loop to run “forever” (until the process is interrupted/terminated).

$\endgroup$
2
  • 5
    $\begingroup$ I think throw shouldn't be allowed as the only exit from such a loop, because other statements like baz(); can also throw. If you have a separate "infinite loop without break" keyword like forever then throw could still be allowed in those loops though. This is just my opinion, but I think if you have separate keywords for the two kinds of loops then the distinction should be that while loops can complete normally (so the code after them is reachable) whereas forever loops can't complete normally (but can still complete abruptly by throwing). $\endgroup$
    – kaya3
    Jul 1, 2023 at 0:02
  • 1
    $\begingroup$ Good point. Designing new syntax (or citing other existing syntax examples) is interesting, but this answer focuses on the specific reason OP finds the original syntax error-prone. $\endgroup$ Jul 3, 2023 at 13:47
10
$\begingroup$

I've sometimes wished for a loop like

do {
  foo();
} while (condition) {
  bar();
}
$\endgroup$
4
  • $\begingroup$ That looks pretty readable to my eyes. $\endgroup$ Jul 1, 2023 at 7:37
  • 1
    $\begingroup$ I see this going really wrong when the do is off-screen reading a longer code example. The while (condition) { bar(); } part would be valid by itself, but now when control reaches that final }, it no longer goes to the "matching" {, but instead to the do { one. The } before while on the same line is a useful contextual clue, but that wouldn't apply in e.g. a language with Python-like braceless syntax. $\endgroup$ Jul 1, 2023 at 16:07
  • 4
    $\begingroup$ This seems elegant to implement. Instead of there being a separate do {} while () construct, there can just be a while () with an optional do {} prefix. $\endgroup$
    – CPlus
    Jul 1, 2023 at 20:59
  • 2
    $\begingroup$ I would be very confused if I saw this in code. Is foo() ran only once before the loop, and then bar() ran at every iteration? Or is foo ran at every iteration, and then bar() is ran only once after the loop? $\endgroup$
    – Stef
    Jul 3, 2023 at 14:34
8
$\begingroup$

In my experience, this "half-loop" occurs for a couple of language-inherent reasons:

  1. foo() needs to mutate some state related to the loop, usually in some fallible way, and bar() needs to non-fallibly get at some of the results; and
  2. there is overlap in the values that mean that foo() failed, foo() succeeded but the result is "empty", or that the loop should end for ordinary reasons

Maybe foo() reads from a device into a single, temporary buffer and returns the number of characters read, but also a value less than 0 if there's an error. So you can't just loop until foo()'s return value is no longer true-like. You also have to do this dance where you copy from the single-read buffer into the full-response buffer, reallocating or incrementing an offset pointer when necessary. You might also have to check to make sure that the buffer shared by foo() and bar() has valid contents, or cruft left over from last time, or uninitialised memory. And so on.

Once this pattern is established out of necessity eg. in stdlib functions, it becomes an idiom and is used even when alternatives are possible.

You can solve this at the language level by allowing users to access and manipulate types that represent data flow in loops. This is made more useful by having good "optional" types so that they have natural sentinel values for ending a loop, invalid values, etc.

Originally, I was going to suggest that the answer to this was something I use a lot in Rust: iterators. This is not original or unique to Rust, it's just the version I'm most familiar with. Python has a similar approach except with a special exception for ending an iterator.

An iterator owns all the state required to produce more values, and it has a next() method which returns Some(item) if there's another item, or None if there isn't. There are other semantics that are possible with more conditions, but that's the crux.

So you take apart your foo() and bar() functions and the underlying thing they manage, and put them in an iterator that "owns" the device or stream or whatever. Inside the next() method you do a single iteration of the read-check-buffer operations, and return Some(data) or None. The different concerns are neatly separated, plus there's support from the stdlib for eg. collecting all the readable data into a single buffer, or chaining subsequent operations.

Perhaps most crucially though: the data returned from next() is available in the loop body if and only if the loop body executes. So there is no confusion around "does this buffer I put on the stack before starting the loop contain data from the last iteration? or invalid, uninitialised data?" - no. If the loop body is running, if there is a value in the item binding, you're good. The operation to advance the loop worked. That's the data you got. If it failed, you won't be in the loop body.

As I said, originally I was going to cite "iterators" as a potential answer, but really it's just one realisation of what I said above: Rust (and Python) programmers now have access to the types that control data flow through loop iteration, because for item in collection is equivalent to turning collection into an iterator and looping until Nones come out. Same with while let. (Rust also has an actual control flow type but the more common Option and Result also have elevated status in the language syntax and are more commonly used.)

$\endgroup$
2
  • $\begingroup$ You have the right idea, but the iterator protocol in Python doesn't care about None; it cares about either raising and handling a StopIteration, or explicitly counting values with a hidden integer value (used for indexing). $\endgroup$ Jul 3, 2023 at 13:50
  • $\begingroup$ @KarlKnechtel I did mention that ("special exception"), albeit briefly. My point still stands. Python more or less has to use an exception because there's no way to express the same kind of structure you might do with static types (eg. Some(None) or Continue(()) being valid values), but it's still a realisation of letting the user dictate both the loop control flow and data flow coupled together. $\endgroup$
    – detly
    Jul 3, 2023 at 14:28
7
$\begingroup$

Instead of having a separate condition expression, have the loop body return a Result, and repeat the loop until it's an error (i.e., "while it succeeds").

Using this style, you can rewrite all sorts of control flows by just changing where in the body the test is performed:

# Traditional `while`:
while {
    test_condition()?
    foo()
    bar()
}

# Loop-and-a-half:
while {
    foo()
    test_condition()?
    bar()
}

# Do-while
while {
    foo()
    bar()
    test_condition()?
}

Where fn()? is a Rust-style operator that immediately returns an error if the call fails. You can think of it as an assert, and the loop stops when the body fails an assertion.

With a little bit of optimizing, it should perform the same as a traditional while loop.

You can also extend it to boolean operators, where a false result is considered an error:

while {
    foo()
    (count < 10)? # or count <? 10
    bar()
}

This is better than the if (!condition) break because the condition can be used inline, and the user is less likely to forget the "break" since there's no obligatory condition at the start.


It can also replace other tricky scenarios, like an if that must operate on an assigned value only if it succeeds. This is a feature most languages need special syntax for, but happens naturally if the condition is part of the body:

if {
    match = re.match('\d+', str)?
    print("Got a number:", match)
} {
    match = re.match('[a-z]+', str)?
    print("Got a word:", match)
} {
    print("No match")
}
$\endgroup$
3
  • 3
    $\begingroup$ I don't know whether to be disgusted or amazed. As elegant as this looks in shorter code, it may be difficult to find the ? in a longer loop, and this is even more error-prone / easy to forget than break. But sure, Bool = Result<Void, Void>, why not. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 13:30
  • 1
    $\begingroup$ @Bbrk24 Haha, that's a great reaction. I have a whole self-hosted language based on this trick, and so far it's been working really well, especially since you can interleave multiple condition tests as needed. But I totally understand the shock. $\endgroup$
    – BoppreH
    Jul 3, 2023 at 14:06
  • $\begingroup$ I really like this concept, but I wonder that separating the condition from the 'while' impedes readability. Eg.: while { ... long procedure... condition? ...more procedure... condition2? } - doesn't read very well. Perhaps renaming from "while" to "loop" or even "loop if" might reduce the disconnect between the syntax of the language and the syntax of English (assuming English). A potential advantage of "loop if" is you then have "loop for" in a matching style. The "if" statement is less bad, but could still be confusing: if {condition1? condition2? foo()}. Is that condtion1 && condition2?? $\endgroup$ Jul 4, 2023 at 1:46
6
$\begingroup$

If your only concern is someone missing a break, you could allow syntax like while(!break), while(!break && !throw) or whatever other loop-exiting mechanisms your language has. It would be equivalent to while(1) except it would be an error not to have the specified means of exiting the loop inside.

Also if your language has named breaks like in C. T. Zahn’s 1974 paper that I’m having trouble locating but that look like the following, perhaps you would be able to use that feature to require a break with a particular name.

for(item in items) {
    if(item.is_interesting()) {
        break found_interesting(item);
    }
    if(item.breaks_invariant()) {
        break epic_fail;
    }
}
when found_interesting(item) {
    do_something(item);
}
when epic_fail {
    crash();
}

Does any language have a dedicated syntax for this?

Forth:

BEGIN part1 WHILE part2 REPEAT

If part1 leaves a false value on the top of the stack, exit the loop. Nothing prevents part1 from doing a lot of other work prior to calculating whether to break.

$\endgroup$
2
  • 1
    $\begingroup$ Is this the paper you're looking for? $\endgroup$
    – Bbrk24
    Jul 2, 2023 at 15:33
  • $\begingroup$ @Bbrk24 Yes, that’s the one. Looks very useful, I wonder why it hasn’t seen any adoption in 50 years. $\endgroup$ Jul 3, 2023 at 16:33
5
$\begingroup$

The following might be highly unidiomatic in many C-like languages, but you did ask "does any language have a dedicated syntax for this", which suggests that you might be looking for ideas from other families.

gcc does tail-call optimisation and C++ has lambdas, so I hope you grant that those are features a C-family language can have. With this in mind, F# would do the following:

let rec loop () =
    foo ()
    if condition then
        bar ()
        loop ()
loop ()

So what you want could plausibly be spelled as follows, where I've invented some syntax

nocapture void loop() {
    foo();
    if (condition) {
        bar();
        tail loop();
    }
}
loop();

I argue that this doesn't have code duplication (unless you count the initial invocation of the loop). I also argue that it's extremely clear what is going to happen when, such that "forgetting the condition" is a very unnatural move.

The downsides:

  • the mild one that it's eight lines rather than five;
  • the substantial one that depending on how the closure is implemented, an insufficiently smart compiler might copy local state to the heap, or it might become much more verbose as you have to specify what state is captured and where. The hypothetical language probably wants specific support for this pattern to make a user comfortable relying on it. (I gestured to this with nocapture and tail, but with an appropriate linter or a sufficiently smart compiler you wouldn't need those keywords.)

If you're designing a language which is happy to have a complicated spec anyway, then I'm sure you could special-case this pattern into the syntax to make it much shorter.

$\endgroup$
1
  • $\begingroup$ Now that you mention it, isn’t this how loop-recur works in Clojure? Having that as a macro or as a keyword pair could work just as well in a C-family language. $\endgroup$
    – Bbrk24
    Jul 1, 2023 at 17:55
4
$\begingroup$

Directly as

loop
   foo;
   exit when not condition;
   bar;
end loop;

in either Ada or VHDL for example.

$\endgroup$
3
  • $\begingroup$ That's what I would have suggested, except that you beat me to it, and I would have recommended outdenting the "exit when" line to match up with "loop" and "end-loop" when there aren't any nested control structures. Perhaps I might also add a syntactic marker that would be required before any statement which contained a statement that would exit the loop, with a recommendation that such a marker be aligned with the start and end of the loop. $\endgroup$
    – supercat
    Jul 2, 2023 at 17:34
  • 1
    $\begingroup$ It's not clear to me what exit when not condition is really doing better (= in a less error-prone way) than if (!condition) break. $\endgroup$ Jul 3, 2023 at 13:41
  • $\begingroup$ @KarlKnechtel mainly that this is what exit is for; break in C has other uses. So in a trivial example, I'd agree both are equally error-resistant, but more complex code with nested if and case statements / switches can become challenging. Both Ada, VHDL of course still have while and for loops, which are just syntactic sugar over this basic loop. (VHDL allows labelling loops, and exit <label> when ... for nested loops) $\endgroup$ Jul 3, 2023 at 15:04
4
$\begingroup$

In some languages, like Dart for example, assignments also return the value. While it's not applicable to all situations, it removes the necessity of a loop-and-a-half in some cases. For example you could write this while loop

String? input = stdin.readLineSync()
while (input != "end") {
    print(input);
    input = stdin.readLineSync()
}

as this instead

String? input;
while ((input = stdin.readLineSync()) != "end") {
    print(input);
}
$\endgroup$
4
$\begingroup$

Does any language have a dedicated syntax for this?

Bourne-shell type languages (sh, bash, zsh, etc.) have it:

while
  foo
  condition
do
  bar
done

To be clear, I'm answering on having a syntax that's specifically dedicated to this type of loop construct.

Other languages like Rust, Lisp-based languages, Ruby, Haskell, or even the same Bourne-shell based languages, will support very similar constructs by way of consequence from having a syntax that generically turns lists of statements into expressions. That enables their use in the loop condition, which is generally limited in most languages to an expression. In bash, that can look like this:

while {
  foo
  condition
}; do
  bar
done

Ruby:

while (foo; condition)
  bar
end

Haskell:

whileM (do { foo; condition }) $ do
  bar

Common Lisp:

(while (progn (foo) (condition))
  (bar))

JavaScript (not sure lambdas are fair, though):

while ((function () { foo; return condition; })()) {
  bar;
}

The while syntax in Bourne-shell type languages however, instead of taking a single command/expression for the condition, will take a list. From the bash manpage:

while list-1; do list-2; done

The while command continuously executes the list list-2 as long as the last command in the list list-1 returns an exit status of zero.

This is why it requires the do keyword to signify the end of one list and the start of the other.

I am aware that this doesn't show a significant functional difference if any for the language user. However, it is the only language I could think of that has a syntax dedicated to this as was asked, and I do think it's interesting to note, at least for this site, because having non-generic syntax rules like this does complicate the language. The idea of the return status of the last command being the return status of a list is not just documented where "lists" is documented, but also here where while is documented. And that has a sort of avalanche effect, because then set -e also has to make a special note on the lists used for while conditions, despite set -e not being about loops.

-e

Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword

$\endgroup$
2
  • $\begingroup$ This seems to be essentially the same idea as in Rust (Kaya's answer). $\endgroup$ Jul 3, 2023 at 13:45
  • 1
    $\begingroup$ @KarlKnechtel I don't think so. The Rust one is following the idea that blocks of statements are expressions and their while syntax accepts an expression. Here, the while syntax specifically states it takes a "list": "list-2 is executed as long as the last command in list-1 returns a non-zero exit status" (bash manpage). This is why it has the do keyword to signify the end of list-1 and the start of list-2. If the syntax were instead to take a command (or pipeline) instead of a list, then it'd need either a {} or () compound command and I would agree with you that it's the same idea. $\endgroup$
    – JoL
    Jul 3, 2023 at 21:58
3
$\begingroup$

How about adding a keyword entry. by re-ordering the loop top have the condition at the end and starting in the middle you get nce clean block with no internal branching.

do{
    bar();
entry:
    foo();
}while (condition())

It's not immediatly clear that bar will be skipped initially, o perhaps a new keyword.

do subsequently
{
     bar();
always: //  coulde be a different keyword like then or default: here instead
     foo();
} 
while ( condition() );

entry: seems like a comefrom, and Intercal officianados excepted I don't think anyone prefers comefrom over goto, so probably cleaner to just use a goto.

goto entry;
do{
    bar();
entry:
    foo();
}while (condition())

or rerhaps replace do with something that explicitly indicate that the loop starts in the middle. do from entry or something like that.

re-ordering the loop to put a middle test last is reminiscent of Duff's device, hence:

switch (0)
{
     do
     {
          bar();
default:  foo();      
     } 
     while (condition());
}

not a seriouis suggestion obviously.

another possible language change could be to enhance for(;;) with a "glue" slot that is skipped on the first loop but othwerwise runs after the condition test.

 for(;condition();;bar())foo();

But for(;;) itself is already failrly complex, and thus can be hard to read,

Back to the original. ( here I'm using for(;;) instead of while(1) because it's idiomatic in C. - substitute while(1) or forever etc if you prefer)

for(;;)
{
    foo();
    if(condition()) break;
    bar();
}

This may actually be a good case for suffix-if

for(;;)
{
    foo();
break if(condition());
    bar();
}

it shouldn't be too hard to teach a code formatter to outdent break if

The outdented break makes the termination condition easy to find. while the leading for{;;} makes it clear that you always get at-least one foo()

$\endgroup$
10
  • 1
    $\begingroup$ duff's device seems odd. but I'm not language lawyer enough to say that it is forbidden. compiles with no warnings and runs fine with --std=c11 what's "modern?" what's C-family for that matter? $\endgroup$
    – Jasen
    Jul 2, 2023 at 3:11
  • 1
    $\begingroup$ I don't tend to consider new editions of C/C++ "modern" because they still show their age in plenty of obvious ways. $\endgroup$
    – Bbrk24
    Jul 2, 2023 at 3:17
  • 1
    $\begingroup$ switch(0) { ... default: ... } is just a silly way to write a goto. (To jump to the loop entry point, as a compiler might do in asm (Why are loops always compiled into "do...while" style (tail jump)?) if it doesn't rotate and partially peel an iteration to put the condition at the bottom.) In a language that supports goto, just write goto with a meaningful label name like loop_entry; it's the same bypassing of structured programming, obfuscating / disguising it as a switch doesn't make it better, except for scoping of the target. $\endgroup$ Jul 3, 2023 at 12:49
  • 1
    $\begingroup$ entry: as a keyword on its own (without a goto) seems like it could be hard to read in less trivial loops. If it's not on screen when you're looking at the do { ... and first few lines of the top of the loop, you won't realize right away that the entry point isn't the top, unless there's a comment on or near the do{ line, or some part of the language syntax indicates a special form of do which uses an entry-point label. Like do(entry) { ... entry: ... } perhaps? Having a choice of label name could let you jump into the middle of a nested do{}while for good or ill... $\endgroup$ Jul 3, 2023 at 12:56
  • 1
    $\begingroup$ This is an interesting exploration of options, but it hardly seems likely to make writing the code easier, more readable or less error-prone. (Also, OP already considered the simple goto approach.) $\endgroup$ Jul 3, 2023 at 13:52
2
$\begingroup$

This seems like a suitable use case for the do-while loop, which is supported in C.

do
{
  foo();
}
while (condition && bar());
$\endgroup$
1
  • 4
    $\begingroup$ This only works if bar() is an expression which always returns a truthy value. You can use condition && (bar() || true) to get around that if its value isn't necessarily truthy, but not when the part of the code represented by bar(); is not an expression. For most use-cases the bar(); part of the "loop-and-a-half" is considerably longer than the foo(); part. $\endgroup$
    – kaya3
    Jul 1, 2023 at 16:19
2
$\begingroup$

A possible syntax for Python-like (braceless, with syntactically-significant whitespace) languages, inspired by Jeremy's answer:

while condition after:
    foo()
do:
    bar()

This introduces an additional after keyword (and probably also do; C-like languages commonly have that, but Python doesn't) to mark the construct explicitly. Assuming this cost is acceptable, I think this gets the best of all possible worlds otherwise:

  • It's eminently readable: "while this condition is met after doing this part, do that part".

  • The loop-controlling condition appears at the top, so it doesn't get lost in other code.

  • Both this sort of loop and an "ordinary" while loop start with the while keyword. For a longer loop, it's clear where control flow resumes at the end of the loop; the code reader won't accidentally ignore the preamble. The "main" part of the loop is no longer a valid loop by itself.

  • We get a do-while construction for free by simply omitting the do: block, and the reader can see the loop condition up front. while False after: tells us immediately that we have the equivalent of do {} while (0) without having to skip to the end. It's also less awkward with this style of syntax, since we can still follow the usual rule that a control-flow statement introduces a block.

$\endgroup$
5
  • $\begingroup$ I'll note that the only time I've seen do { ... } while (false) is in C/C++ macros, where it's useful to group multiple statements as if they're a single statement and require a semicolon at the end. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 13:43
  • $\begingroup$ Admittedly I've never felt a compelling use for them in Python, but it does come for free here so I thought I'd point it out. The idea is that break can be used as a pseudo-goto to a common end point. Alternately, it gives some of the functionality of Javascript IIFEs. $\endgroup$ Jul 3, 2023 at 14:09
  • $\begingroup$ The mian reason I've used IIFEs is to put multiple statements where a single expression is needed, e.g. in Swift var button: UIButton = { let button = UIButton(); button.color = .blue; return button }(). Making it out of do/while doesn't help with that. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 14:16
  • $\begingroup$ It does if break is allowed to produce a value analogous to return, but that is admittedly a separate feature request :) $\endgroup$ Jul 3, 2023 at 14:23
  • $\begingroup$ The more I think about it, the more I like this idea. The after bit could be a contextual keyword -- and therefore still allowed as an identifier -- if you require parentheses around the condition, as C does. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 15:07
2
$\begingroup$

I saw this maybe 40 years ago in a book in a library, as a suggestion, not in an actual library. Normally we have loops with optional break statements. So the syntax for this loop was like:

while (<condition>) until <exit1>, <exit2>, <exit3> {
    ....
    if (x > 0) exit1;
    if (y > 0) exit2;
    if (z > 0) break;
    ...
    if (x == 0) exit3;
    ....
}
case case1: { statement; break; }
case case2: { statement; break; }
case case3: { statement; break; }

So after the loop condition comes "until" with one or more names for exit cases. The exit names can be used in your code just like break. And after the loop body it's like the cases of a switch statement. You can easily distinguish between "break" and the loop running to its end by using an exit name instead of break.

Of course this includes an n + 1/2 loop but is much more general.

$\endgroup$
2
  • $\begingroup$ This looks similar to Roman Odaisky's existing answer. $\endgroup$
    – Bbrk24
    Jul 3, 2023 at 14:24
  • $\begingroup$ Knuth included a version of this in Literate Programming, as I recall, but the version I remember didn't include the initial while (<condition>) clause, because the if statements can easily cover that clause: The entire loop simply read as until this, that, theOtherThing { ... } and then if statements could exit it as needed. (until forever { ... } reads nicely, too, for a loop that never ends.) $\endgroup$ Aug 4, 2023 at 20:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .