The While-Wait-Do Idiom, by cntkillme: Addressing while wait() loops

while wait() do end has an extra opcode, which is testcall. Luau optimizes out testcall from while true do end, though.

2 Likes

I didn’t even realise this was an idiom but it does make me wonder whether or not I genuinely have a faulty understanding of some concepts within Lua itself.

(sorry about the reply to you pysephDEV, I wasn’t trying to reply to you directly, just to the article)

Since the web archive doesn’t really work with Google documents, and due to the fear that this might be lost to time, I have decided to copy the document in this thread, properly formatted.

Archived by @iSpaceRBLX, 8th March, 2021, 01:17 AM EET

Document

The “While-Wait-Do” Idiom
Matthew I. (CntKillMe)
Written September 2018

Introduction
First and foremost, this is a document about a Roblox-specific concept that plagues experienced and inexperienced developers alike. If you don’t do scripting within Roblox, this article is probably not going to be useful.

It is very popular among the community to “shorthand” while loops by expressing the condition of the while loop with (or containing, typically as part of a conjunction) a function call to wait. At first glance, this seems rather innocent: an extra line is usually saved and the code is often just as readable. However, the drawbacks of such an idiom completely dominate all assumed (often inaccurately) benefits and can result in illogical coding habits.

Not following this idiom has become almost a stigma in Roblox. There will always be a group of people telling you that you can and should be calling wait within the conditional portion of the while statement as opposed to in the body. The “apparent” benefits to this typically masks the problems encouraging such an idiom results in. Newcomers are taught to do this from an early stage and it infects their habits in a somewhat significant way.

Benefit - “Less code is cleaner [or more readable] code”
This is by far the most popular rationale behind why people prefer to use this idiom. I cannot deny that this appears to often be true when taken at face value, but it is really only true when less code results in less complexity, which is not always the case. If we define cleaner in a way that reasonably minimizes line count, then sure, you have an argument. You sometimes save what on average is about 1 line per all applicable while loops (more on this later). However cleaner is best defined as something that is more simple and maintainable (which typically yields more readable code). A few extra lines typically will not affect readability, however abusing the purpose of the conditional portion of a while loop is clearly less simple. When you call wait in the condition for whether the loop should continue, you make the implication that the return value is somehow important in determining whether or not the loop should terminate. Except that in all scenarios where this happens, the return value of wait is irrelevant. The fact that it returns anything at all is irrelevant, we just abuse the fact that wait does return something that is neither false nor nil. This is clearly less simple code, because we introduce an unnecessary test for our condition when we already know it will never be false or nil. The entire purpose of the conditional part of a while loop is to determine when the loop should terminate. When while true do is read, you understand it is as an infinite loop immediately. True is always true, which makes it obvious that the loop does not necessarily have a terminating condition. When while f() do is read, the general idea is the loop will not terminate until f returns a falsy value. while wait() do then, when directly interpreted, means the loop will terminate once wait returns a falsy value. The problem is this idiom alters our perspective and makes an exception to this rule, we no longer treat the terminating condition as a terminating condition when wait is called. We learn to associate while wait() do as a non-terminating loop. Clearly an unnecessary distinction, and one that can be quite confusing for beginners who have not yet come across this idiom. Although the added complexity is quite small, the pitfalls that come about utilizing such an idiom can be quite alarming (more on this later).

Benefit - “Less code is faster code”
Most people already know this is false, and admittedly this rationale is not often used as an argument in favor of this idiom. However I have come across quite a few people who believe this to be true. Of course it is not true, the performance difference between the two are beyond negligible that outside noise is likely to make more of an impact. It is unfortunate that people always fall accustom to premature optimization, but it is these people that argue in favor of this idiom just for this reason. If they can be pedantic, so can I. Technically, following this idiom actually results in slower code as the return value of wait must be tested. However Lua does a small optimization when certain truthy constants are used as the condition, no value testing will need to be done and instead it will fall straight through into the loop’s body (see luaK_goiftrue in lcode.c). Of course testing a value is quite quick, so for me to tell you to not to use this idiom because it is slower would make me a victim of premature optimization. Truth is, there is practically no difference and it is not even worth thinking about.

Benefit - “Everyone uses it”
This rationale is perhaps the only real benefit. In general, it is a good idea to follow the same conventions everyone else uses to write more consistent and easily-understood code. Unfortunately, not all popular conventions are sound. There is no doubt that many have become accustomed to this idiom, but following it is bad practice. Needlessly increasing the complexity of our code in such a way is just unnecessary and following this idiom almost always redefines what the conditional part of a while loop is even meant to be used for.

Non Maintainability
Perhaps the most obvious problem is how the idiom strips our freedom by forcing us to yield at the start of each iteration rather when it may be more appropriate. From a functionality standpoint, this seems to be hardly a problem. However by staying consistent with this convention, the return value of wait will always be lost. Normally that is fine, however in some cases it is very useful to know how long the thread has actually been yielded for. Typically if logic needs to be adjusted precisely over time, the return value of wait can be important to ensure consistent logic across fluctuating yield times. In such a case, the call to wait will be moved into the body. In the worst case, wait is called again within the body because the idiom is too deeply rooted that it becomes impossible to imagine a while loop that does not call wait in its conditional part. As stupid as this might sound, it is a somewhat common mistake that occasionally affects newcomers. By consistently pushing this idiom, beginners see it to be a good convention to follow and stick to it. Before they even fully understand while loops they are writing code that abuses it.

Bad Habits
Since following this idiom results in a sort of mindset change that makes abusing while loops like this okay, it becomes easy to forget what the purpose of the conditional part even is. All too often code is written in the following manner:

while wait() do
    if ((p1 - p2).Magnitude <= 100) then
       break
    end
end

instead of in the correct manner:

while (p1 - p2).Magnitude > 100 do
    wait()
end

Actually utilizing while loops correctly instead of sticking with this idiom can easily increase simplicity, readability, maintainability, and (to a miniscule extent) efficiency. This is not just a trivial example, a lot of people write similarly to the first example all the time. I was at one point a “While-Wait-Do Idiom Follower” as well. Time and time again I would see people spouting nonsense like “while true do crashes your place, always do while wait() do instead.” Or when people were being convinced to follow this idiom if they have not already because some established group of people (i.e. active forumers and popular developers) were doing so.

Too Long, Didn’t Read
I follow this idiom for:

  • Readability reasons. No.
  • Maintainability reasons. No.
  • Performance reasons. Read benefit 2.
  • Conventional reasons. Get over it.
  • Because I can. Congratulations! You’re a bad programmer.

I don’t follow this idiom for:

  • Readability reasons. Okay.
  • Maintainability reasons.Yes.
  • Performance reasons. Read benefit 2.
  • Conventional reasons. You’re a rebel. :metal:
  • Because I can. Congratulations! No one cares.
2 Likes

The document allows copies. You can create a copy of it in the File menu if you’d like a personal copy in your Google Drive, or you can duplicate it onto a site that does work well with WebArchive. Gists might be a good place for that but there’s many options out there.

2 Likes