You don't need ifs nor conditional jumps. Functional calls are truly enough. I can show you how it's done, if you want to see it.
Thus you can implement 'if' as a user defined function in eg Haskell.
(But I'd advise against the totally if-less style. Pattern matching is just too convenient. (And ifs are just a special case of pattern matching on booleans.))
Pure lambda calculus doesn't I have ifs. If you want to go truly crazy, check out SKI calculus. It doesn't even have lambda abstraction.
If I may guess: you want to make gotos central? That only holds for compiling to von-Neumann machines. Some other computational structures might not even have instruction pointers to move around.
This is relevant. There are lots of real-life errors that come out of these errors and the worse mistakes people make trying to work around control flow (goto fail?).
I desperately want a 'break break' command. Not a label break, which is painful to work with, but an instruction to break the current loop and the outer loop.
That works well except when you're in a language where you can't nest functions and need data from the outer scope. C is like that.
Rust really likes to bail out of functions when something goes wrong. That's what "try!()" does.
Allowing multiple exits from a loop is no longer controversial. Multiple entries into a loop are still considered undesirable.
From a proof of correctness standpoint, the topology requirement is that there be some single place within the loop through which control must pass. That's where the proof inductive step takes place, and where you prove loop termination by showing that some measure is decreasing. Restrictions stricter than that are more stylistic than formal.
Ah, you seem to be
talking about having the source code of function
X inside the source code of
function Y and, then, the names
known in function Y are also
known in function X unless
declared again within function
X. I LIKE that! That's what PL/I
has.
Once I got a phone interview from
Google. An early question was,
"What is your favorite programming
language?"
Sure, right away, PL/I! Opps, (from a movie) "way
wrong answer!". Apparently the
only right answer was C++. Gee,
I didn't want to lie. Besides, to me
saying C++ instead of PL/I should
cause me to lose a full letter grade!
So, PL/I has descendancy, static
and dynamic. The static version is
from the nesting in the static source code. The
dyanmic version is from functions,
subroutines, etc. that have been
called (are active) but have yet to
return.
Then with such descendancy and entry variables, can get
some interesting situations, design
patterns!
I did that once and avoided a
total mess in the IBM AI language
KnowledgeTool!
I don't like the goto fail thing being discussed as an example of goto being evil. If they had used the other common pattern - returning -1 on failure - instead of the pattern they used, their code would look like this:
if (a)
return -1;
if (b)
return -1;
return -1;
if (c)
return -1;
return 0;
And literally the exact same issue would occur, even though goto wasn't used.
I love all those "goto" disguised as any kind of flow controls. Because goto is devil, but give it another name and another smell and, voilà!, it becomes beautiful.
At a hardware level all control flow is made up of gotos. But by writing them as 'for' or 'break' or 'if' in our programs we make them easier to read by letting the reader know that the jump is restricted in certain ways.
Nah, hardware is more like dataflow or flow-based programming. Maybe functional programming, too. Goto is jumping to arbitrary, imperative code in RAM with an address and no regard for current state. There's possibly something in hardware like it but default stuff I saw in books were more like FSM model focused on data flows.
Yes, I know :) . Having learnt to program some microprocessors, I love how everything is evaluated in terms of jump-if-zero, jump-if-not-zero. Well, even some cracks to copy-protection from older games (dating myself here) was just a single change from a hex 74 to a hex 75 somewhere in the code.
And, sometimes, even in small programs, knowing where you are after some jumps is a messy thing.
I just don't have all that rant against jumps (or goto's). I know where they live, what they eat, when they are useful and when are not. :)
I encountered one of these beautiful things you mention not too long ago.
void foo()
{
do {
for (...) {
if (...) {
break;
}
}
if (...) {
...
if (...) {
...
break;
}
}
/* even more nested loops and conditionals with break sprinkled all over */
} while (0);
...
}
PS: IMHO this code goes too far with the "goto considered harmful" philosophy. The "while (0)" is serving the purpose of a label. An explicit label instead would be more readable, IMHO.
Smells bad, but could legitimately be the correct structure. Depends on the problem and performance constraints. If you have a lot of conditionals and it's too expensive to run them all, nested ifs are correct. If you need to break from conditions arising from a multiple indexes, then nested loops are correct.
Dogma has no place in coding. Best practices give you a sense of smell. They don't tell you how to factor.
You would never use nested if statements for performance reasons. The conditions in any modern language will short circuit, which will give the equivalent behavior as manually making nested loops. Depending on the circumstances it might be more readable, but never more performant.
Nested if-statements lets you avoid re-evaluating the conditions between if-statements. Short-circuit evaluation only partially addresses that.
It can be cleaner to cache the conditions in a few flags if you want to do it that way, though. I'm not saying deeply nested conditionals are pretty, just that there are some cases where they're quicker. (Of course, if you're scraping the bottom of the barrel for that kind of speedup then chances are, there are algorithmic improvements that will yield orders of magnitude more gains.)
Depending on the specifics, an alternative is to break that "while(0)" loop into a function and use "return" instead of break.
Even more of an alternative (and would only be a good idea in certain circumstances) is, in a language with destructors, to make an object created at the top with a destructor for that last block of code, so you can throw out the loop and use return instead of break. This would also work with Golang defer statements. But again, only a good idea if that makes some sort of sense in your program, and isn't just a work around for the lack of gotos.
Gotos make me cringe, a "while(0)" hack makes me cringe just as much. The whole problem with goto is it confuses the reader as to where the control flow is going, but this is just as bad because until you read to the end you're under the impression those statements will be run multiple times.
Yes. Because giving it a different name addresses the problem with goto.
The problem with goto is that it is opaque. It has no structure, it has no constraints, and it communicates no intent. It says "execute the code behind this label," and gives no clue what the code is or what it does.
But if you give goto a different name that reflects how you're using it, that solves the problem! If your goto is named "break" and it occurs inside a loop, that tells you what the goto is trying to achieve. It tells you at a glance what code you expect to find when you jump, and its relationship to the other code in the program. In short, it communicates intent.
The same is true when the name is "continue" or "throw": you know where in the program that goto leads and what sort of code you expect to find when you go there. You don't need to read it to find out. And if you do read it, you have a better idea of whether it's right or not.
(Also note that criticism of goto applies a lot more to historical gotos, whose argument was an arbitrary memory address. Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.)
> The same is true when the name is "continue" or "throw": you know where in the program that goto leads
> Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.
That doesn't actually apply for your examples of "continue" or "throw" though. "continue" has no label, numeric or symbolic; its target is implicit from the lexical scope. "throw" has a sort of label (the type/class of the given value/exception), but its target is determined by the dynamic environment. Given a function like:
function foo() {
throw Bar;
}
There is nothing in the lexical environment (i.e. the blocks within which it is written) that tells us where this will jump to. That information depends on what the stack looks like at the call site, which may be written by someone else, on the other side of the world, 20 years from now.
I apologize if I wasn't clear, I didn't mean to say that continue/throw are similar to modern gotos. Renamed gotos such as continue, break, and throw address the primary drawbacks of raw gotos. Unrelatedly, the gotos that one is likely to encounter in a modern-ish language like C or Common Lisp don't have all of those drawbacks anyways. My purpose was not to draw a comparison, merely to add some additional context for Dijkstra's original rants that may explain why the hard-line stance he took at the time makes a little less sense today.
That's true, although 'return' will only pop one frame off the stack whilst 'throw' will keep going until it finds a handler. Hence 'return' has a more predictable/consistent behaviour, and less context-dependence.
Heh, I actually wrote something about continuations, then decided against it. I was going to say:
In a pure CPS setting, the caller's "only" responsibility is to give the right arguments (in scare quotes because getting a continuation right is usually trickier than the "plain" values usually given to functions).
In languages with functions, the caller is also responsible for handling/ignoring any return value and carrying out any subsequent computations, if the procedure returns.
In languages with exceptions, the caller is also responsible for handling/passing-on any exceptions.
Out of these three sorts of language, my current preference is for the second. It's probably fair to say that most procedures in a codebase will return a value (i.e. they're not continuations) and will not throw an exception (they're exceptional, after all). Hence it seems empirically desirable to allow `return`: it alters the semantics from a pure CPS scenario to one where we must always be vigilant that a procedure may return; but since most procedures (in such languages) return, that's not an edge case (in fact it's continuations, procedures without `return`, that are rare!); hence the benefits seem worth the cost.
When `throw` is introduced, we again must be vigilant when we're calling procedures. However, since most procedures don't `throw`, the benefit of having this ability is less than that of `return`. Also, since there may be many occurrences of `throw`, with different exception types, the cost of looking out for and handling these is greater than for `return`.
Hence my (current) opinion that `return` is very much a "good GOTO", whilst `throw` is a "bad GOTO" :)
The trick with throw is, of course, assuming that any procedure may potentially throw, except for a selected 'always succeeding' few. So avoid any non-local mutation and for those that are unavoidable take a transactional approach.
'call-cc', the control operator from hell that puts 'spaghetti' goto to shame.
> Some people seem determined to cause facial tics in everyone they meet.
Maybe so, but not me.
So, what is the justification for the Do Forever loop above?
First, I illustrate that we can want to exit the loop for (A) whatever conditions are discovered in the loop and (B) at any point in the loop and not just the beginning or end which is the usual situation, e.g., for
Do i = 1, 10
or
Do While X
If I write
Do I = 1, n
does the loop logic deciding when to exit ever form n + 1? If n is the largest integer, then maybe we'd like to know. With Do Forever we may have more control, enough more to avoid forming n + 1 unless we really want to.
Second, when I start writing a loop, I may not have yet worked out all the details or how to get what I do want just from some
Do While X
Etc.
So, I just start the loop with Do Forever and work out the exit conditions as I keep typing.
Third, with Do Forever can often get the results of using a GOTO without actually doing so. So, if the target of the GOTO would be just after the loop End, then can get there within the loop with just a Leave.
Of course, when write such a loop with Do Forever, should write some comments that explain (A) what the purpose of the loop is and (B) how the code in the loop does accomplish that purpose. That is, should not force someone reading the code to guess at (A) and confirm (B).
For proofs of correctness based on the usual mathematical induction, I'm not convinced that loops with Do Forever are fundamentally more difficult.
> Some people seem determined to cause facial tics in everyone they meet.
When I teach this stuff, I try to distinguish between
(1) Returning/Exiting/Excepting because of a legitimate error condition
(2) Returning/Breaking early because there's either no work to do or something necessary to do the work is absent/unavailable (the latter often turns into a case of (1)
(3) Returning/Breaking/Continuing early because I've discovered the work is done for either all iterations or this particular iteration, and the remainder of the function/method/loop is pointless cycling. (Often added during optimization, sometimes added too early).
(4) Returning/breaking/GOTOing because I've gotten confused and want to go to a place in the code that makes more sense to me. This is the only that's actually a problem and should be railed against. Unfortunately, it's way more common than it really should be in beginners' programs. I think GOTO got an early bad rap---try writing non-trivial machine code or assembler without liberal use of goto, and many very useful techniques in debugging are basically informed application of goto---because of what I call the "pull the ejection handle" approach to getting into trouble trying to code an algorithm you dont really understand.
Just use functions calls already. Modern languages and compilers / interpreters have gotten good enough (at least the ones that care), that there's no need to bless a small set of control structures by being special-purpose built-in.
Goto gets a bad rap, and I have seen many programs where an explicit goto, to a label that is descriptive, is way clearer (and less brittle under reorganization) than a 'structured' break or continue - or some spaghetti of booleans (while (!done) ... ).
It's clearly very easy to write bad code in many ways, but (very) judicious use of goto is occasionally far clearer than assuming goto is always bad and building something complex out of bools, break and continue. It seems to be a test of common sense: if goto is clearer than the alternative, use goto.
Your specific example sounds like the kind of thing addressed by more powerful structure: labelled break, as appears in Java, JS, Rust and Swift (among others, I'm sure).
Of course, thinking about labelled break is just dreaming when required/choosing to work in a language without it.
It's why some of us built 4GL's, DSL's, preprocessors, and so on that added stuff to such languages but extracted to purely that language. Mine was a BASIC with macros translated to C w/ safety checks.
Unfortunately, in my experience the abuse of labelled breaks to avoid gotos tends to be even worse than abuse of regular breaks, since the label no longer shows immediately where the flow goes; it only labels the start of the block, when what you really want to see is the end.
I guess that could indeed be annoying for reading code, especially if the labelled block/loop is very long (I personally usually end up using these for short nested loops, longer loops usually get broken out into separate functions, and the biggest teams/programs I've worked on haven't been using languages that have this feature). That said, the extra structure/more restrictions of labelled breaks lends itself to compilers and other tooling being more helpful, both for optimisations (although SSA + basic blocks that most compilers now use makes this somewhat irrelevant) and for catching mistakes while, for instance, refactoring.
In any case, I strongly disagree that labelled breaks aren't better than regular breaks: the chains of booleans required otherwise are very annoying and error prone.
Labelled break is strictly less powerful than gotos, and IMO its implementation in Java is far inferior to the block Common Lisp special form (https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node85.html) because s-expressions make the lexical scoping unambiguously visible.
IMO there is no good substitute for gotos when writing transpilers (and certain interpreters) and low-level concurrency code. The Common Lisp prog macro is the best way that I have seen to use gotos (https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node91.html).
Of course it is strictly less powerful than gotos. That's actually the point of the more structured constructs: they are more restrictive and so can be reasoned about more easily. Note that I'm specifically piking up on labelled break/continue as a substitute for nested loops like the following
while ... {
bool shouldBreakOuter = false;
while ... { ... shouldBreakOuter = true; break; }
if (shouldBreakOuter) break;
}
since that what the original comments description sounded like. Being a rather general control flow mechanism, there are of course other uses of goto which this doesn't cover.
That said, I'm not sure I understand the comparison: the lexical scoping doesn't seem to be ambiguous in Java. Could you expand on what you mean?
If you visualize nested scopes as indent levels, the labelXYZ: sits in an indent level above where it is visible. Not only that, but labelXYZ: does not come before the statement that it transfers control to, but two statements before. If all that was not bad enough, most people format the labeled statement to sit at the same indentation level. The combination of these three things is pretty much the worst possible when it comes to reading code. Compare this to block/tagbody/prog and their indentation conventions in Common Lisp.
goto is best viewed as a low level (unsafe) operation.
I wouldn't mind it if the language expressly made me call such a function from a library of other 'unsafe' operations. Optional things that you can include when you really know what you're doing and use them in a limited way.
It's one of those tools that is powerful, dangerous, and best avoided when another will suffice. Yet like explosives, sometimes dangerous is the best tool for a job.
In the case of a programming language goto can be used to emulate features and/or paradigms that a language doesn't have or doesn't quite do sufficiently. It should ALWAYS have a comment that annotates the intent.
How is goto an inherently unsafe operation? As long as the usage is clear, it's as safe as any other logical instruction. Problems pop up when nobody else is able to read your code and fix a bug. I feel bad because it has been drilled into my head from the start that goto is unconditionally bad. So I never use it. But I saw a friend replace my spaghetti jungle of break and continue with a simple and clear flow using goto. It was a real mind blowing moment.
It's 'unsafe' like how working with raw pointer arithmetic is 'unsafe'. It escapes the paradigms offered to provide regular flow and might also require the compiler to restrict how it handles those sections of code. As others have pointed out elsewhere, it also affects how humans interpret the code, thus it is more likely to be related to bugs as well.
It is not remotely unsafe the way that raw pointer arithmetic is unsafe. Proving things about what raw pointer arithmetic winds up doing is undecidable; non-computed goto, on the other hand, is completely obvious. On the theory side, at worst you can create nasty irreducible flow-graphs where you can't simply put code on 'edges', but this is pretty minor. It is very unlikely that a compiler is going to go into the weeds from a local goto; you are spreading FUD.
Exceptions, on the other hand, or non-local goto, has a good chance of seriously scrozzling compilers and causing a lot of optimizations to go away.
All that being said, the potential to break human understanding of the code with gotos is serious.
I've been programming for a long time, and have come to the conclusion that "structured programming", then "object-oriented programming", and now "functional programming" --- and whatever comes next --- are all manifestations of dogma: "best practices" that someone obviously thought was a good idea to solve many problems, but not all. The difference between "many" and "all" is where these "best practices" turn into horrible common-sense-violating abominations.
I think programming should really be taught starting with Asm, and thus, a language where gotos are the norm. Flowcharting should be considered an essential skill for designing and describing algorithms. Control structures like if/else, do/while, while, for, etc. should be taught as abstractions that are to be used where it would simplify the code (much like all abstractions should be used for), but if your algorithm doesn't fit these structures 100%, it's better to use gotos than to try to "force" it to fit by creating a "spaghetti of booleans".
State machines are another use-case where goto makes perfect sense: instead of storing state in additional variables, the current position in the code is enough. It makes debugging much easier when you are actually jumping between the states as you step through the code, rather than looping back up into a switch or a series of if/else checks on another variable.
Its really not that hard. Instead of blindly following rules someone else wrote, just read the code. Is it easy to follow? How many details do you have to juggle in your head? How many indented scopes are there? How much state is there? Can you mostly read it from top to bottom, or do you constantly have to jump around the file? If someone unfamiliar with the code (maybe you in 3 months) needs to make a change, how easy would it be for them to overlook some important detail and mess it up?
Best practices usually apply in general, but the smart person who coined them cannot know all possible situations and the parrots who blindly repeat them don't know what they're talking about. Think for yourself, know your tools, and use them effectively to make your code as simple as possible.
61 comments
[ 2.4 ms ] story [ 118 ms ] threadThus you can implement 'if' as a user defined function in eg Haskell.
(But I'd advise against the totally if-less style. Pattern matching is just too convenient. (And ifs are just a special case of pattern matching on booleans.))
Pure lambda calculus doesn't I have ifs. If you want to go truly crazy, check out SKI calculus. It doesn't even have lambda abstraction.
If I may guess: you want to make gotos central? That only holds for compiling to von-Neumann machines. Some other computational structures might not even have instruction pointers to move around.
I desperately want a 'break break' command. Not a label break, which is painful to work with, but an instruction to break the current loop and the outer loop.
Rust really likes to bail out of functions when something goes wrong. That's what "try!()" does.
Allowing multiple exits from a loop is no longer controversial. Multiple entries into a loop are still considered undesirable.
From a proof of correctness standpoint, the topology requirement is that there be some single place within the loop through which control must pass. That's where the proof inductive step takes place, and where you prove loop termination by showing that some measure is decreasing. Restrictions stricter than that are more stylistic than formal.
Once I got a phone interview from Google. An early question was,
"What is your favorite programming language?"
Sure, right away, PL/I! Opps, (from a movie) "way wrong answer!". Apparently the only right answer was C++. Gee, I didn't want to lie. Besides, to me saying C++ instead of PL/I should cause me to lose a full letter grade!
So, PL/I has descendancy, static and dynamic. The static version is from the nesting in the static source code. The dyanmic version is from functions, subroutines, etc. that have been called (are active) but have yet to return.
Then with such descendancy and entry variables, can get some interesting situations, design patterns!
I did that once and avoided a total mess in the IBM AI language KnowledgeTool!
And, sometimes, even in small programs, knowing where you are after some jumps is a messy thing.
I just don't have all that rant against jumps (or goto's). I know where they live, what they eat, when they are useful and when are not. :)
Dogma has no place in coding. Best practices give you a sense of smell. They don't tell you how to factor.
It can be cleaner to cache the conditions in a few flags if you want to do it that way, though. I'm not saying deeply nested conditionals are pretty, just that there are some cases where they're quicker. (Of course, if you're scraping the bottom of the barrel for that kind of speedup then chances are, there are algorithmic improvements that will yield orders of magnitude more gains.)
Even more of an alternative (and would only be a good idea in certain circumstances) is, in a language with destructors, to make an object created at the top with a destructor for that last block of code, so you can throw out the loop and use return instead of break. This would also work with Golang defer statements. But again, only a good idea if that makes some sort of sense in your program, and isn't just a work around for the lack of gotos.
Gotos make me cringe, a "while(0)" hack makes me cringe just as much. The whole problem with goto is it confuses the reader as to where the control flow is going, but this is just as bad because until you read to the end you're under the impression those statements will be run multiple times.
The problem with goto is that it is opaque. It has no structure, it has no constraints, and it communicates no intent. It says "execute the code behind this label," and gives no clue what the code is or what it does.
But if you give goto a different name that reflects how you're using it, that solves the problem! If your goto is named "break" and it occurs inside a loop, that tells you what the goto is trying to achieve. It tells you at a glance what code you expect to find when you jump, and its relationship to the other code in the program. In short, it communicates intent.
The same is true when the name is "continue" or "throw": you know where in the program that goto leads and what sort of code you expect to find when you go there. You don't need to read it to find out. And if you do read it, you have a better idea of whether it's right or not.
(Also note that criticism of goto applies a lot more to historical gotos, whose argument was an arbitrary memory address. Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.)
> Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.
That doesn't actually apply for your examples of "continue" or "throw" though. "continue" has no label, numeric or symbolic; its target is implicit from the lexical scope. "throw" has a sort of label (the type/class of the given value/exception), but its target is determined by the dynamic environment. Given a function like:
There is nothing in the lexical environment (i.e. the blocks within which it is written) that tells us where this will jump to. That information depends on what the stack looks like at the call site, which may be written by someone else, on the other side of the world, 20 years from now.Unless it was a tail call...
Really, throw and return are quite similar, both invoke an externally provided continuation and abandon the current one.
In a pure CPS setting, the caller's "only" responsibility is to give the right arguments (in scare quotes because getting a continuation right is usually trickier than the "plain" values usually given to functions).
In languages with functions, the caller is also responsible for handling/ignoring any return value and carrying out any subsequent computations, if the procedure returns.
In languages with exceptions, the caller is also responsible for handling/passing-on any exceptions.
Out of these three sorts of language, my current preference is for the second. It's probably fair to say that most procedures in a codebase will return a value (i.e. they're not continuations) and will not throw an exception (they're exceptional, after all). Hence it seems empirically desirable to allow `return`: it alters the semantics from a pure CPS scenario to one where we must always be vigilant that a procedure may return; but since most procedures (in such languages) return, that's not an edge case (in fact it's continuations, procedures without `return`, that are rare!); hence the benefits seem worth the cost.
When `throw` is introduced, we again must be vigilant when we're calling procedures. However, since most procedures don't `throw`, the benefit of having this ability is less than that of `return`. Also, since there may be many occurrences of `throw`, with different exception types, the cost of looking out for and handling these is greater than for `return`.
Hence my (current) opinion that `return` is very much a "good GOTO", whilst `throw` is a "bad GOTO" :)
Then we get `call-cc`... ;)
'call-cc', the control operator from hell that puts 'spaghetti' goto to shame.
Some people seem determined to cause facial tics in everyone they meet.
Maybe so, but not me.
So, what is the justification for the Do Forever loop above?
First, I illustrate that we can want to exit the loop for (A) whatever conditions are discovered in the loop and (B) at any point in the loop and not just the beginning or end which is the usual situation, e.g., for
or If I write does the loop logic deciding when to exit ever form n + 1? If n is the largest integer, then maybe we'd like to know. With Do Forever we may have more control, enough more to avoid forming n + 1 unless we really want to.Second, when I start writing a loop, I may not have yet worked out all the details or how to get what I do want just from some
Etc. So, I just start the loop with Do Forever and work out the exit conditions as I keep typing.Third, with Do Forever can often get the results of using a GOTO without actually doing so. So, if the target of the GOTO would be just after the loop End, then can get there within the loop with just a Leave.
Of course, when write such a loop with Do Forever, should write some comments that explain (A) what the purpose of the loop is and (B) how the code in the loop does accomplish that purpose. That is, should not force someone reading the code to guess at (A) and confirm (B).
For proofs of correctness based on the usual mathematical induction, I'm not convinced that loops with Do Forever are fundamentally more difficult.
> Some people seem determined to cause facial tics in everyone they meet.
Maybe so, but not me.
2. Is the code relatively readable by someone unfamiliar with the problem?
If 1 && 2 then stop tearing your robes like Pharisees and go solve another problem.
It's clearly very easy to write bad code in many ways, but (very) judicious use of goto is occasionally far clearer than assuming goto is always bad and building something complex out of bools, break and continue. It seems to be a test of common sense: if goto is clearer than the alternative, use goto.
Of course, thinking about labelled break is just dreaming when required/choosing to work in a language without it.
In any case, I strongly disagree that labelled breaks aren't better than regular breaks: the chains of booleans required otherwise are very annoying and error prone.
IMO there is no good substitute for gotos when writing transpilers (and certain interpreters) and low-level concurrency code. The Common Lisp prog macro is the best way that I have seen to use gotos (https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node91.html).
That said, I'm not sure I understand the comparison: the lexical scoping doesn't seem to be ambiguous in Java. Could you expand on what you mean?
I wouldn't mind it if the language expressly made me call such a function from a library of other 'unsafe' operations. Optional things that you can include when you really know what you're doing and use them in a limited way.
It's one of those tools that is powerful, dangerous, and best avoided when another will suffice. Yet like explosives, sometimes dangerous is the best tool for a job.
In the case of a programming language goto can be used to emulate features and/or paradigms that a language doesn't have or doesn't quite do sufficiently. It should ALWAYS have a comment that annotates the intent.
Exceptions, on the other hand, or non-local goto, has a good chance of seriously scrozzling compilers and causing a lot of optimizations to go away.
All that being said, the potential to break human understanding of the code with gotos is serious.
I think programming should really be taught starting with Asm, and thus, a language where gotos are the norm. Flowcharting should be considered an essential skill for designing and describing algorithms. Control structures like if/else, do/while, while, for, etc. should be taught as abstractions that are to be used where it would simplify the code (much like all abstractions should be used for), but if your algorithm doesn't fit these structures 100%, it's better to use gotos than to try to "force" it to fit by creating a "spaghetti of booleans".
State machines are another use-case where goto makes perfect sense: instead of storing state in additional variables, the current position in the code is enough. It makes debugging much easier when you are actually jumping between the states as you step through the code, rather than looping back up into a switch or a series of if/else checks on another variable.
Best practices usually apply in general, but the smart person who coined them cannot know all possible situations and the parrots who blindly repeat them don't know what they're talking about. Think for yourself, know your tools, and use them effectively to make your code as simple as possible.