Can we allow `while` loops without the `do`?

The Scala 3 docs ( Dropped: Do-While ) say

The syntax construct do while is no longer supported. Instead, it is recommended to use the equivalent while loop. For instance, instead of

do
  i += 1
while (f(i) == 0)

one writes

while
  i += 1
  f(i) == 0
do ()

That’s great and all - I’ve been doing this for some time before since the while-construct allows definitions in the loop preceding the condition to be used in the final conditional, whereas do-while does not. However, could we just allow the while without the silly do () at the end?

while
  i += 1
  f(i) == 0

end was granted the power to close empty class bodies.

class C:
end C

Perhaps

  while
    i += 1
    i < n
  end while

Does if also benefit? For what they call uniformity.

  if
    i += 1
    i < n
  end

end need not care whether it is ending a block introduced by colon or keyword.

I would interpret that as an infinite loop

while true do
  ...

where true do, was elided. Not your interpretation where do () was elided.

1 Like

I think it’s much clearer to use the do.

I also think it’s moderately clearer to use {} than (), since {} is clearly an empty block while () is the unit value, placed in a position that doesn’t return a value so it’s silently discarded.

Functionally, they’re equivalent. Conceptually, the logic is “no code block”, though.

In general, one might want the test anywhere.

boundary:
  while true do
    preamble
    if !test then boundary.break()
    postamble

This is the general form of a loop that has an arbitrary entry point. But we don’t need boundary/break to express it:

while
  preamble
  test
do
  postamble

I prefer to see all the parts, even if they’re empty, especially since little tweaks can convert one to the other.

while
  item = xs(i)
  !isCorrect(item)
do
  i += 1
while
  item = iter.next
  if !isCorrect(item)
do
  {}
1 Like

That is backward. The empty braces require insertion of parens. Scala has no empty blocks.

scala> def f = {}
[[syntax trees at end of                     typer]] // rs$line$2
package <empty> {
  final lazy module val rs$line$2: rs$line$2 = new rs$line$2()
  final module class rs$line$2() extends Object() { this: rs$line$2.type =>
    def f: Unit =
      {
        ()
      }
  }
}

def f: Unit

scala> def g = ()
[[syntax trees at end of                     typer]] // rs$line$3
package <empty> {
  final lazy module val rs$line$3: rs$line$3 = new rs$line$3()
  final module class rs$line$3() extends Object() { this: rs$line$3.type =>
    def g: Unit = ()
  }
}

def g: Unit

scala> while 1 > 2 do {}
[[syntax trees at end of                     typer]] // rs$line$4
package <empty> {
  final lazy module val rs$line$4: rs$line$4 = new rs$line$4()
  final module class rs$line$4() extends Object() { this: rs$line$4.type =>
    val res0: Unit =
      while 1 > 2 do
        {
          ()
        }
  }
}


scala> while 1 > 2 do ()
[[syntax trees at end of                     typer]] // rs$line$5
package <empty> {
  final lazy module val rs$line$5: rs$line$5 = new rs$line$5()
  final module class rs$line$5() extends Object() { this: rs$line$5.type =>
    val res0: Unit = while 1 > 2 do ()
  }
}

The idea that leaving out the do clause means the same as do () does have precedence elsewhere in the language, e.g. leaving out the finally and else clauses mean roughly the same as finally () and else (). I don’t think there’s that much precedence for leaving out the trailing clause of a control flow structure means the earlier clause is removed, so hopefully the proposed behaviour wouldn’t be too confusing for people

1 Like

That is a fair point. However, that already assumes that the natural shape of a while loop is

while
  cond
  block
do
  body
  block

and that’s not my experience. I have some while loops like that, but the overwhelming majority are of the form

while condExpr do
  body
  block

Therefore, my expectation of the shape of a while loop is the latter. If it’s missing a do, it must be that condExpr do that went away, not the body block.

Contrast that with an if/then/else. There, the vast majority of them look like

if condExpr then
  thenBlock
else
  elseBlock

and therefore, if an else is missing, I indeed expect the else elseBlock to have gone away.

Likewise for try/finally. The expectation is that it is

try
  tryBlock
finally
  handler

It could be written

try tryExpr finally
  handler

but that’s not the expectation. If it were the most common shape of try/finally block, then I would assume

try
  something

to mean that the tryExpr finally had been elided. But it’s not the case.

4 Likes

Honestly I find both of them really hard to parse, there is nothing to distinguish the exit condition from the rest, and so at a glance I would probably read it as an infinite loop like @sjrd

This is why we had the do-while syntax in the first place

If we had to improve the syntax around these kinds of loops, I’d much rather have something that works in all cases and highlights the condition.

Something like the following (very much WIP):

looping:
  preamble
  while_true test // or `break_if !test` or `until !test`
  postamble

Here is an example with existing keywords to see what it would look like with syntax highlighting:
(But we can’t use these, as that would be ambiguous)

while do:
  preamble
  break if !test
  postamble

But I’m not sure we should encourage people to use while loops in the first place, and so I’m hesitant to add syntax to make them better

3 Likes

Yeah, that’s what I did in kse (runnable):

// Assuming we start with var i = 0
scala> loop:
     |   i += 1
     |   loop.stop(i > 3).?
     |   println(s"The value is $i")
     | 
The value is 1
The value is 2
The value is 3

(I’ve standardized on ? as the universal jump symbol–still trying to figure out whether an opaque jump object with .? or a method like stop_? is clearer. I always have warn on value discard on–otherwise the method is definitely superior.)

Since I adjusted and indeed embraced idiomatic

try expr finally more

and

try expr catch handler // just a function

I think the argument from “expectation about shape of format” is not strong.

I’m recently less sanguine about one-liner

if condition then println() // side effect way on the right

so my opinions are fluid and open to influence from reviewers.

I sympathize with “highlight the condition”. But since we’re waxing nostaglic for goto, maybe the real evil all along was blocks.

Perhaps instead of “[Do] Gather ye rose-buds while ye may,” our grammatical model for while should be the verb, as in the Scarecrow’s lyric: “I could while away the hours, conferring with the flowers.” Then it’s much more natural for while to take a block. I don’t have an answer for making the result expression of the block more visually distinctive, though I have proposed in some context that end supply a value:

while
  f()
end i > 3

I may have suggested for excessively long methods

def f() =
  def preamble() = ???
  begin
    stuff()
  end result() // in lieu of terminal return result()
  def moreDefs() // but no eagerly evaluated exprs

Or was it end f with result. Then, end while with i > 3.

Since double semi was rejected for line comment, we still have available the true idiom for while true, namely

for ;;
  f()

thereby eliminating that ambiguity for do-less while. Maybe more idiomatic for Scala:

for _ <- _ do
  g()

Of course this is true technically.

However, semantically, {} means “this is a block but it does nothing”. (), in contrast, means “nothing to return, here is a placeholder instead”.

So even though {} actually implies (), and if you think it through, just inlining the () seems simpler, I prefer {} as a semantic indicator for what I’m thinking.

(Which is “this is an empty code block” not “here’s a placeholder value for you”.)

Thank-you, you have given me food for thought, namely, if blocks are evil, does that also mean that empty blocks are evil?

I won’t argue over our preference for brackets that are pointy or smooth, but I do wonder, not unseriously, if there is a missed opportunity for the equivalent of uninitialized (for underscore syntax) or ??? (just to mean not implemented).

def f(): Unit = ; // nothing came to mind

To me this was true in the past. But now with Scala 3 doing away with do-while loops, all my do-while loops have become while-do () loops with large while blocks and degenerate do blocks!

In addition to the small number of existing while-do () loops from before (e.g. because things like do { val x = … } while (x > 0) were not allowed, but while {val x = …; x > 0} do () were), these while-do () loops now make up perhaps ~half of my while-do loops overall, and are no longer a negligible edge case as they were before