It easy to misunderstand what he was even talking about because the paper is so short, assumes you know about this context, and has no concrete examples. People quite reasonably assume it's about ugly code they've encountered, but it's actually about ugly code of a completely different kind.
I'm not that old, but I was unlucky enough to have programmed in an unstructured language where GOTO was the only way to use faux-subroutines in my teens. Whatever you think as code that's difficult to follow: it's nothing compared to this.
I grew up with MSX-BASIC: https://github.com/plattysoft/Modern-MSX-BASIC-Game-Dev/blob... – GOSUB jumps to a specific line number (RETURN returns from where it jumped). Even a fairly simple and clean example like this can be rather difficult to follow.
Malicious spaghetti involves transformations such as
for (x = 0; x < 10; x++) {
for (y = 0; y < 20; y++) {
printf("%d %d\n", x, y);
}
}
|
| DRY
v
x = 0;
loop_x_head:
condition_val = x;
condition_stop = 10;
condition_var = 'x';
goto check_condition;
loop_x_body:
y = 0;
loop_y_head:
condition_val = y;
condition_stop = 20;
condition_var = 'y';
goto check_condition;
loop_y_body:
printf("%d %d\n", x, y);
increment_val = y;
condition_stop = 20;
increment_var = 'y';
goto increment;
loop_y_end:
increment_val = x;
condition_stop = 10;
increment_var = 'x';
goto increment;
loop_x_end:
halt;
increment:
increment_val++;
if (increment_var == 'x')
x = increment_val;
if (increment_var == 'y')
y = increment_val;
condition_val = increment_val;
condition_var = increment_var;
check_condition:
if (condition_val < condition_stop)
goto pass_condition:
if (condition_var == 'x')
goto loop_x_end;
if (condition_var == 'y')
goto loop_y_end;
pass_condition:
if (condition_var == 'x')
goto loop_x_body;
if (condition_var == 'y')
goto loop_y_body;
[1] http://wigfield.org/RND_HAR.BASTo find an example, I did a search for “Commodore PET Basic programs”.
Here’s a book from 1979 that shows what spaghetti code looks like, in my opinion:
http://www.1000bit.it/support/manuali/commodore/32_BASIC_Pro...
The first program listing is on page 24 of the PDF. Try to follow the logic of the program. Why does line 400 go to 280? What paths can lead to line 400? Who knows! And this is high-quality BASIC by 1979 standards — it’s in a printed book after all.
There’s an auxiliary listing after the program itself explaining the routines and variables used, but many/most programs in those days wouldn’t have this level of rigorous documentation. Deciphering the program would probably have to start by drawing a flowchart of execution paths.
Might be why I'm good at restructuring horrible to read spaghetti code.
I don’t think that’s true, certainly not for books of that time period. Because the whole field was changing rapidly, writers would often work under tight schedules, and customers would buy about anything because they only had magazines and books to learn from and review sites didn’t exist.
I also think that’s bad Basic for the time. Certainly, comment lines before subroutines would help.
It’s like an iceberg of bad code: the underwater part nobody saw was astonishingly terrible by modern standards. That code might be running a business, but its author would never get exposed to professional programming. Today Excel often serves a similar purpose. (Excel isn’t spaghetti though since the execution model is completely different.)
You wouldn't want to waste characters on commenting code: machines of that era would have only a few KB of RAM, as low as 1K. For the same reason you don't want to waste characters on long, meaningful variable names or on well spaced code. Multiple statements per line isn't to save print space, it's to save RAM.
Meanwhile, the program's pretty well structured for such a short bit of BASIC: subroutines start at multiples of 100, for example, and each subroutine starts and ends clearly, no shenanigans like jumping from the middle of one sub to another, no multiple exit points for subs, all as linear as it can be. The use of IF is limited to skipping forward a short way to conditionally execute a line or two only. GOTO only exists in those IF statements.
I'd have been happy to have written code like this, back then.
I am pretty sure that my uncle ran this exact program on his computer and printed out biorhythms on listing paper, in the mid 80s.
There is a simple thing. On a lot of machines only spaghettified programs would even fit in the memory available. Academic CS researchers with their unlimited accounts on the institutions mainframe didn't have that worry.
That code looks normal to me. I have a ton of BASIC books and magazines. You're talking about a time period before full screen text editors were a thing. It's almost impossible to explain to anyone that didn't have to work with TI-99 BASIC, C64 BASIC, GW-BASIC/BASICA, etc. what it was like. Once you got to QBASIC/QuickBASIC it was done. Life was easy. A few years before that, and you're printing out pages on a dot matrix printer and going line-by-line to debug. You'll notice a distinct lack of white space between lines and that comments start with "REM" and a line number. You didn't even get labels for lines. The code looks like that because those were technology limits on really rudimentary devices. You were editing code inside the BASIC interpreter, often using some command like "LIST <line#>". It was awful.
We take so much for granted today. Dual monitors. Color. More than 80x25 character display. Multiple screens and multitasking. Just getting to Linux in '95 and having F1/F2/F3/etc. switching terminals was a huge deal.
I remember chasing bytes with short variable names, abbreviated print statements, reducing spaces as much as possible etc.
I had like 28k to play with and I was 14 years old!
10 for i = . to 6.28 step 0.1:next
and it would be slightly faster and smaller than 10 for i = 0 to 6.28 step 0.1:next
Made for some ugly inner loops, but you gotta do what you gotta do. For that matter, we certainly would have removed some of those extra spaces as well. Bytes mattered and whitespace slowed you down.gwbasic had a function to relabel all the lines. And the labels skip by 10 numbers exactly for the purpose of inserting lines. Then when you had hit the limit you'd ask the computer to relabel them in steps of 10 again.
gw-basic was released c4 years after this book, and there was a huge change over that period:
1976 - Release of Apple I
1977 - Release of Apple II / Commodore PET
1979 - This book
1982 - Commedore 64
1983 - GW Basic
This book is pretty much closer to the Apple I than gw-basic. Perhaps I should have specifically said developing basic in 1979 though as referenced in that book though (and there isn't just one sort of basic - there are so many dialects).
Without looking at the post-program material, this isn't exactly a difficult question to answer.
Line 400 is preceded by some print statements:
370 PRINT "PRESS 'E' TO END, SPACE TO CONTINUE"
380 GET R$:IF R$="" THEN [goto] 380
390 IF R$="E" THEN [goto] 120
400 L=0:GOTO 280
So we have a prompt that says "press E to end, space to continue", and then branches one of three ways: if you provide no input, the prompt is shown again; if you provide an E, the entire program restarts from scratch, and if you do anything other than that, the count of lines drawn on screen is reset to 0 and the next 18 lines of the chart are drawn.We can assume that line 400 will be hit whenever a piece of chart is drawn to the screen.
The program's structure here is a nested loop: there is a loop between lines 280 and 400 (displaying the chart indefinitely, 18 lines at a time) containing another loop between lines 300 and 360 (displaying 18 lines of a chart, one line at a time).
Why is this supposed to be an example of spaghetti code?
400 L=0:GOTO 290
This would be almost impossible to debug. /* 280 */
do_something();
for (;;) {
/* 290 */
/* display chart in blocks of 18 lines */
/* 400 */
L = 0;
}
when the correct code is this: for (;;) {
/* 280 */
do_something();
/* 290 */
/* display chart in blocks of 18 lines */
/* 400 */
L = 0;
}
The bug is that the call to do_something() precedes the outer loop when it should be inside the loop.Is that easier to debug in C than it is in the BASIC program? What's the difference?
Exactly! Recently I had the "pleasure" to work with some FORTRAN IV code from the early 60s, so I know what you mean. No functions/subroutines, only GOTOs. Even loops were done with labels. There is also a weird feature called "arithmetic IF statements" (https://en.wikipedia.org/wiki/Arithmetic_IF). Luckily the code was pretty short (about 500 lines including comments).
There’s no good way to do a do…while loop in Fortran, other than a goto.
INTEGER A(4,4), C, R
...
DO 10 WHILE ( C .NE. R )
A(C,R) = A(C,R) + 1
C = C+1
10 CONTINUE
Note that Fortran has evolved significantly over time. This is how you would write the same in Fortran 77: INTEGER A(4,4), C, R
...
C = 4
R = 1
DO WHILE ( C .GT. R )
A(C,R) = 1
C = C - 1
END DO> There seems to be a unfortunate inconsistency in the naming of this thing.
Well, Fortran is older than C, so you cannot really blame them :-)
Can't highlight this enough. The type of spaghetti code "goto considered harmful" was reacting to is basically impossible to create anymore, so anyone who didn't work on that type of code in the 80s or earlier probably hasn't seen it.
And thus, is applying the mantra "goto considered harmful" incorrectly. (Such as trying to avoid it in C for clean error handling, when there's no reason to avoid that.)
To try to replicate that experience, you'd have to write your entire application (including all libraries since libraries were often not a thing) as one single function in C. All of it, no matter how many tens of thousands of lines of code, all in one function. Then label every line. Then picture having GOTOs going every which way to any of the labels. For instance you'd preset the counter variable to some desired value and jump right into the middle of a loop elsewhere. Over time you'd surely accumulate special conditions within that loop to jump out. And so on. It's difficult to even imagine code like this today (or in the past 30 years).
Code with extreme branching, while dealing with state/boolean parameters that determine flow branching; error handling that can also create other branches of execution; all of that is really hard to keep in mind when reading such nightmare codebases...
If I look back it always comes back to naming and managing names of things. GOTO 100 is meaningless and one eventually runs out of meaningful names for GOTO labels. For me OOPs addressed the naming issue relatively effectively by using the data type as a namespace of sorts.
Structured programming wasn't about eliminating jumps, it was about enforcing discipline in their use. The simplest way to do that is to eliminate raw GOTO from the language, but it's also possible to just be careful and use it wisely.
I dont fully agree with Dijkstras argument. For example I think early returns can often improve the readability of the code. But worth noting Dijkstra is not primarily concerned about readability but rather about how to analyze the execution of a program.
http://www.u.arizona.edu/~rubinson/copyright_violations/Go_T...
Additionally, far more often than not, continue/break allow you to avoid another form of complexity, bugs, and low comprehensibility: deeply nested conditionals.
I agree that CONTINUE and BREAK are easier to reason about because you can look at them and instantly know what they do without having to look up what label they're jumping to.
The point is that CONTINUE and BREAK jump to exactly one location given their lexical context and cannot jump anywhere else. They are also only meaningful when applied to structured control flow. The problem with GOTO is the unbound nature of its jump target, which leads to control flow that is difficult to comprehend by looking at the lexical structure of a function.
It's a subtyping problem, and you have the is-a relationship backwards: a cat is an animal but not every animal is a cat. GOTO is a BREAK (could always be substituted for one), but a BREAK is not a GOTO.
When you need a BREAK you could implement that in terms of GOTO, but no amount of coercion will allow you to use a BREAK as a generic GOTO.
You wrote this backwards, but you seem to understand the relationship and that GOTO is more general. That is, every BREAK is a GOTO (because you can always substitute a GOTO), but every GOTO is not a BREAK (i.e., you can't substitute a BREAK for some GOTOs because BREAK cannot jump to an arbitrary label).
A GOTO is just one possible implementation of BREAK, just as a cat is one possible implementation of an animal.
The practical impact of this is that it is incorrect to ascribe to BREAK the same weaknesses as GOTO, because BREAK is not a GOTO.
What makes break/continue (including labelled variants a la Java) useful is the fact that the restriction on where they can jump means that the control flow graph is guaranteed to be reducible. That is not the case with free-form goto.
The distinction matters because the whole premise of Dijkstra's argument is that if you replace the GOTO keyword with a bunch of more limited versions that cannot be used to produce spaghetti, code quality would go up. The only way for that to work is for the language to be semantically incapable of expressing GOTO.
As I said in my other reply, you seem to have the subtyping relationship wrong: GOTO is a subtype of BREAK (anywhere you find a BREAK you could replace it with GOTO), but BREAK is not a GOTO (you cannot do the reverse).
I agree that BREAK/CONTINUE are not syntactic sugar in languages that don't have GOTO.
Type X is a subtype of type Y if and only if an instance of X can always be used where an instance of Y is required.
GOTO can always be used to replace a BREAK. Therefore GOTO is a subtype of BREAK.
BREAK cannot always be used to replace a GOTO. Therefore BREAK is not a subtype of GOTO.
The inheritance relationship here is not single, it's multiple: a GOTO is a BREAK, but it is also a CONTINUE and a whole lot of other things. It's like a monster class that inherits from every interface under the sun and can do just about anything.
Dijkstra was basically advocating for refactoring our languages to extract those capabilities into smaller, more focused keywords (as well as dropping most of the functionality). Rather than having one keyword implement both the BREAK and CONTINUE interfaces, we break them out into separate keywords.
At this point, I don't care which you call the subtype. You can claim that as a win if you want, but having spent so much time on this stupid thread I think we've both lost.
I'm still interested in exploring the idea, but you're welcome to tune out at any point.
> the set of operations that can be done with GOTO is a superset of those that can be done with CONTINUE or BREAK
Yes! And this is actually part of my point. If Y is a subtype of X, then the set of valid operations on Y is a superset of the set of valid operations on X. This is true for any types, by the definition of subtyping.
This means that you're absolutely correct that the set of operations GOTO can perform is a superset of those BREAK can perform, and for this very reason GOTO is a subtype of BREAK.
The reason why I'm focused on the types and not the operations is because the question at hand has been whether BREAK has the same flaws as GOTO. My argument is that this hinges on whether or not BREAK is just a type of GOTO.
CONTINUE, BREAK, and GOTO all operate predictably because they are deterministic operations. Each continues program execution at the directed explicit (goto) or implicit (continue or break) offset. There is no non-deterministic or unpredictable behavior whatsoever.
If I sat you in front of a computer generating numbers using a pseudo random number generator and gave you as context the last number it generated, could you make any prediction about the next number it generates?
Now if it used a prng that was known and standardized to only compute one number could you predict anything about the next number now?
Seems like a reasonable trade for the occasional "goto again".
[Before anyone reads the above as advocating MISRA C ---- I think MISRA actually tells you not to do "goto fail;" which is advice I'm kind of dubious about. It also tells you to not do "good = good && side_effecty_thing();" (no shortcutting operators when there are side effects) so its style has you make a typical function absolutely littered with explicit initialization guards.]
Its still quite possible in assembly, where goto (JMP) is your only way to do control flow. But I doubt there's many people left who write and maintain large assembly programs. I imagine most programmers reach for C or something higher level as soon as the program becomes non-trivial.
I still use this cute online Intel 4004 simulator sometimes when I teach programming:
Its a fun challenge for novice or advanced programmers alike to write little programs in assembly for a CPU from 1971. The assembly language[1] is only 45 commands, and you only need a handful of them anyway. The CPU interpreter is simple enough you can literally see it think.
Yes, certainly very possible in assembly as it ever was. But as you note, few people are doing large scale assembly programs anymore. And I'd say that those who still do, are sufficiently experienced to avoid unstructured jump explosion in their code, hopefully.
Dijkstra would clearly disaprove of this use of goto. But he would blame the C language for making it necessary. Languages with structured cleanup (like the using clause in C#) does not need to use gotos for resource cleanup.
Dijkstras argument does not distinguish between short and long gotos or long or short functions. His argument applies to any use of goto.
Forty years later, I'll still use a C goto if the situation warrants (e.g. as a getout from deep but simple if). Maybe because having long been an assembler programmer as well, goto's are part of the landscape (if/else is effectively a conditional and unconditional branch/jump).
Loops would look like:
int i = 0;
loop_start:
print i;
i = i + 1;
if (i < 10) goto loop_start;
Fizzbuzz would look something like: int i = 0;
loop_start:
if (i % 5 != 0) goto not_fizzbuzz;
print "fizzbuzz";
goto done;
not_fizzbuzz:
if (i % 3 != 0) goto not_fizz;
print "fizz";
goto done;
not_fizz:
if (i % 5 != 0) goto not_buzz;
print "buzz";
goto done;
not_buzz:
print i;
done:
i += 1;
if (i < 100) goto loop_start;
Often, this wasn't just constrained to a single function; the whole program would be constructed like this, with gotos which jump back and forth across pages and pages of code. Languages wouldn't even have a call stack with subroutines (which is why "procedural" languages -- languages with procedures -- were important enough to be given a special name).At least that's my understanding of it. I haven't lived through this, and my only experience with this kind of stuff is writing assembly, where we always make use of a call stack, so even that is in practice a procedural language. If I have gotten anything wrong, please correct me.
loop_test: IF (NOT loop_condition) GOTO after_loop
loop_body
GOTO loop_test
after_loop: etc...https://github.com/Keith-S-Thompson/fizzbuzz-c/blob/master/f...
#include <stdio.h>
#include <setjmp.h>
int main(void) {
jmp_buf jb[7];
volatile int j = 0;
setjmp(jb[0]);
volatile int i = 1;
if (j == 0) setjmp(jb[1]);
if (j == 1 && i > 100) longjmp(jb[6], 0);
if (j == 1 && i % 15 == 0) longjmp(jb[4], 0);
if (j == 1 && i % 3 == 0) longjmp(jb[2], 0);
if (j == 1 && i % 5 == 0) longjmp(jb[3], 0);
if (j == 1) printf("%d\n", i);
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[2]);
if (j == 1) puts("Fizz");
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[3]);
if (j == 1) puts("Buzz");
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[4]);
if (j == 1) puts("FizzBuzz");
if (j == 0) setjmp(jb[5]);
i ++;
if (j == 1) longjmp(jb[1], 0);
if (j == 0) setjmp(jb[6]);
j ++;
if (j < 2) longjmp(jb[0], 0);
}OTOH early FORTRAN had procedures with arguments and results, but no recursion.
Structural or not was also not necessarily all-in. FORTRAN and BASIC both had for-loops before they had structured conditionals.
I would say js/python callback-based frameworks of today like Twisted (also c++/rust futures) is exactly that
Just like 8 bit BASIC spaghetti code, only refined.
Was that a Casio calculator? Because it was like that for me, only GOTOs existed. Learning about C and seeing these things called loops was a revelation because I had reinvented them with GOTOs already in my programming.
GOSUB was so much worse than GOTO in that it had a stack for the current line but no stack for variables so you could not write recursive functions. I think Fibonacci as a recursive function is malpractice but boy was it a hassle to write Quicksort in BASIC although I had no trouble coding up an FFT (1950s FORTRAN style) from first principles in BASIC on TRS-80 Model 100 on a bus ride across Vermont.
Funny though I did come to a conclusion that for the Arduino programs I wrote I didn’t need a stack at all.
However, while I sympathise with a C programmer who feels goto is necessary in their language, I think that speaks to a problem in the language rather than the programmer. If you have better structural support in your language you can express the things the author (of the link, not Dijkstra) wants to express without needing this unstructured go-to.
For example Rust's break 'label value; allows us to mark any compound expression with the 'label, and then say from anywhere inside that expression but nowhere else that we've decided the value of the expression overall and here's what it is.
This doesn't feel that different from what is being done here with goto, except for two crucial things as a result of being structured:
1. Rust will type check this, if this region of the program picks a Dog, our break needs to provide a Dog, it can't just shrug and expect the program to continue without one. This means maintenance programmers don't need non-local reasoning, this region of the program does, in fact, always pick a Dog, albeit the break 'label value is something to look closely at if you're reading that region itself.
2. We cannot do this, even by mistake (e.g. as a result of copy-paste) across scopes. If you try to break 'label result from the cat care loop into the dog loop earlier in the same function, that just doesn't compile, whereas the C goto has no problem attempting that (a good C compiler should notice if you try to do something really egregious, but good luck).
Dijkstra is clearly arguing for a “single entry single exit” style. But modern consensus seem to uphold single entry but accept multiple exits from a block. Break, continue, early returns, exceptions - all are example of multiple exit. These are more constrained than gotos but nevertheless Dijkstras argument applies to them also.
I personally belive early returns can greatly improve readability (when not nested too deep) an that exceptions are typically cleaner than the alternative. But I acknowlede Dijkstra would disagree.
Break, continue, and early returns always return to the end of the block, unlike goto in the '70s. Exceptions are more complicated, and the cause of a lot of inunderstandable programs.
[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
Sure, but that has nothing to do with whether the GOTO statement should be used. The semantics of GOTO are arguably quite clear; they amount to setting a different continuation for the running program. (Semantically, this implies that the precondition of the GOTO statement is made a possible precondition for the label that the statement jumps to; and execution simply does not proceed to the next statement.)
There are even "relooper" algorithms to reconstruct a structured program from patterns in the idiomatic use of GOTO statements: they are used in compiling to structured object languages such as WASM code. Using GOTOs in an idiomatically sensible way (avoiding spaghetti code) may be less readable than writing actual structured blocks, but only slightly so.