1. 17
  1.  

    1. 10

      That Djijkstra quote is great.

      We should do our utmost to shorten the conceptual gap between the static program and the dynamic process.

      It feels like a good way to explain and justify a bunch of approaches feel correct, but would otherwise be purely preferential. One I’m particularly fond of is arranging code in natural reading order from top to bottom rather than the opposite (C style). There the static program and dynamic process seem in sync.

      1. 13

        What’s the natural reading order, though? If you read the top routine first, you don’t know what the subroutines do yet, so you can’t fully understand the function until much later, when you actually read the subroutines. And if you read the subroutines first, you don’t know what they are used for, and why you’re even bothering.

        What happens in practice is that we start at the top level, and then constantly look up the API of each subroutine it uses (assuming it is properly documented, my condolences if you need to dig up the actual source of the subroutines).

        That ultimately is the “natural reading order”. Not top down, not bottom up, but jumping around as we gather the information we need. And I don’t know of any way to write code that linearises that. The best we can do is have our IDE pop up a tool tip when we hover the subroutine name.

        1. 6

          I think the main thing that’s missing from this article is a discussion of the value of guarantees. If I know the code doesn’t use goto, then it usually makes it much easier to read, because I don’t have to constantly be asking “but what if the flow control does something unexpected?” which is really distracting. You give up a certain amount of flexibility (the ability to jump to an arbitrary point of code) and in return you gain something much more valuable (code becomes easier to read and understand). Languages without early return offer similar advantages.

          Reading order has the potential to offer a similar (but admittedly less valuable) guarantee. If you have to define something before you use it, then the answer to the question of “where is this thing I’m using defined?” is always “up”, which I have found to be very helpful. After getting used to this, I feel somewhat disoriented when reading code where it isn’t true.

          1. 4

            Huh, I thought the consensus was that single-exit restrictions turned out to be a bad idea, because early exits allow you to prune the state space that needs to be considered in the following code. If you don’t have early exit, you have to introduce a variable to keep the state.

            The main argument for single-exit was to make it easier to work with Hoare logic, but I think later formal systems were better at modelling more complicated flow control.

            1. 1

              I haven’t heard that myself, but I should specify that I’m talking about entire languages designed around not having early returns (scheme, ocaml, erlang, etc; basically languages without statements) rather than using a language designed around having early returns and banning their use.

              1. 3

                Yeah, the problems happen with the combination of the single-exit restriction and a statement-oriented language. Expression-oriented functional languages allow you to return a value from a nested if without having to unwind the nesting first.

                1. 3

                  I think the core problem is join points.

                  In an expression oriented language, your conditionals branch out into a tree, and every branch returns a value, so there are no join points. If you need a join point, you use a (possibly local) helper function.

                  In a statement oriented language, you can do the same thing, where each branch of your conditionals ends in a return. If you don’t, if you let the conditionals fall through to a common statement, now you can’t “look up” and there’s no “coordinate” (loop variable) that helps you orient yourself. Doing single-return pretty much forces you to have join points.

                  While loops also create join points, but the loop condition helps orient you.

                  I think that when break and continue are confusing, it’s more about the join point (I don’t know what’s true after the jump) than the jump itself.

            2. 1

              Huh, I thought the consensus was that single-exit restrictions turned out to be a bad idea

              You can still return early without goto. As for the error handling goto enables, that’s mostly just C missing a defer statement. If you stick to the usual patterns, it’s just a variation on early returns.

              1. 1

                I was referring to languages with loops but without things like break or continue, and when return can only appear at the end of the function.

                1. 1

                  To be honest your comment felt like a non sequitur: the comment you were replying to was making no mention of single-exit functions, either as a coding rule or as a language restriction, so I wasn’t quite sure why you brought it up.

                  Edit: reading Technomancy’s comment for the fifth time, I finally caught that little detail: “Languages without early return offer similar advantages”. Oops, my bad.

        2. 3

          The full quote in Goto Considered Harmful is:

          My second remark is that our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.

          For me, when I talk about natural reading order, I’m referencing the general idea that we read left to right, top to bottom (modulo non-LTR languages). In programming, we use abstractions all the time (modules, classes, methods, variables, etc.).

          What’s the natural reading order, though? If you read the top routine first, you don’t know what the subroutines do yet, so you can’t fully understand the function until much later, when you actually read the subroutines. And if you read the subroutines first, you don’t know what they are used for, and why you’re even bothering.

          I find that with reasonable intention revealing names, the first problem you list (that you cannot fully understand the function without reading every subroutine in it) is rarely a problem, because that’s the entire purpose of programming (to abstract unnecessary details in a way which makes it easy to understand large systems.

          What the Dijkstra quote highlights succinctly to me is the idea that spatial and temporal equivalences have value in understanding software. Aligning things code which is defined spatially before with code which happens temporally before is a natural and reasonable approach. The opposite causes the need to read generally downwards when tracing the temporal nature of code, but upwards whenever it calls some sub part of the code. That to me is unnatural.

          That ultimately is the “natural reading order”. Not top down, not bottom up, but jumping around as we gather the information we need. And I don’t know of any way to write code that linearises that. The best we can do is have our IDE pop up a tool tip when we hover the subroutine name.

          It’s that jumping around that’s a problem. The spatial directionality of the jump being aligned with the temporal is relevant. You can make the same argument about this as an argument against inheritance in OOP. It’s effectively the same problem, temporal effects after are defined spatially before, which is inherently more difficult to visualize.

          I’ve been tempted to write a clippy lint for rust that checks that methods in files are arranged topologically sorted (either top down or bottom up depending on the user’s preference). There’s nothing too difficult about the algorithm there. If A calls / references B, then the definition of A belongs before/after (per pref) B in any source code where they’re defined together.

          All that is to say, I recognize that there are elements and rationales for the bottom-up (particularly long held practices, consistency, and familiarity), but where those rationales don’t exist, top-down should be preferred (in my subjective opinion - mostly based on reading / writing code for 30+ years).

          1. 1

            For me, when I talk about natural reading order, I’m referencing the general idea that we read left to right, top to bottom (modulo non-LTR languages)

            I figured that much. I just got past that, and to the problem of writing programs that match this order.

            I find that with reasonable intention revealing names, the first problem you list (that you cannot fully understand the function without reading every subroutine in it) is rarely a problem

            Let’s ignore my first paragraph, which I dampened in my second: “What happens in practice is that we start at the top level, and then constantly look up the API of each subroutine it uses”.

            I can believe that you rarely have to look up the actual source code of the functions you use (if your code base has a good enough documentation, which in my experience has been far from systematic). But I don’t believe for a second you don’t routinely look up the documentation of the functions you use. It has been my overwhelming experience that function names are not enough to understand their semantics. We can guess the intent, but in practice I need more certainty than that: either I know the semantics of the function, or I have to look them up.

            Even if all your functions are exquisitely documented, that doesn’t prevent you from having to jump around to their declaration — either manually, or by having your IDE pop up a tooltip.

            because that’s the entire purpose of programming

            X is true because that’s the purpose of Y is rarely a good argument. First you have to convince me that Y fulfils its purpose to begin with. As for programming specifically… I’ve seen things, and I don’t have much faith.

            What the Dijkstra quote highlights succinctly to me is the idea that spatial and temporal equivalences have value in understanding software.

            So far I agree.

            Aligning things code which is defined spatially before with code which happens temporally before is a natural and reasonable approach.

            It is above all a flat out impossible approach. Not just because of loops, but whenever you call a function, the documentation of that function is either above the current function, or below. And that’s if there’s a documentation at all: many internal subroutines (including a good portion of mine) are utterly devoid such.

            Unless the name of the function is enough to reliably guess its purpose, which is often not the case at all, you have to look it up. Or down. And then you need to jump back. So unless you can write your subroutine right there where it’s called (something I actually encourage when the subroutine is called only once), you can’t avoid breaking the natural reading order. Because program flow is fundamentally non linear.

            You can make the same argument about this as an argument against inheritance in OOP. It’s effectively the same problem, temporal effects after are defined spatially before, which is inherently more difficult to visualize.

            Aahh, you may be conflating things a little bit. The real problem with inheritance, and many bad practices for that matter, is less the reading order than the lack of locality. I have an entire article on locality, and inheritance is fairly high on the list of things that breaks it.

            Now sure, having a way to organise your program that makes it more likely that you find the code you need to read, even if it doesn’t decrease the physical distances between two related pieces of code, does decreases the mental effort you have to spend, which is the whole point of locality to begin with. I still believe though, that keeping code that is read together written together is more important than choosing which piece should be above or below — as long as it’s consistent.

        3. 3

          What’s the natural reading order, though?

          Exactly. It’s different for different people, and also different depending on what you’re actually looking for in the program you’re reading.

      2. 4

        As a mathematician, I find it much more natural to define things before using them.

    2. 5

      After another read, I maintain the understanding that Dijkstra’s rant pretty much only applies to intra- inter-procedural jumps, which are not enabled by the (imo infrequently useful yet completely benign) goto of most languages in use today. I can see why this fairly fundamental communication mismatch has resulted in the industry’s obsession with this essay. (Although I may be out of touch as I seem to generally work with languages that inherently render readability within a single function body to be entirely unimportant.)

      Oh, also note that clang correctly warns about the uninitialised count in the first example, but gcc cares not.

      1. 2

        I think you meant inter-procedural (between different procedures) not intra-procedural (within the same procedure).

        It’s true that goto often worked as a longjmp in higher-level languages when that essay was written, but it isn’t clear to me that Dijkstra is complaining about that. He’s annoyingly vague; he wrote,

        One can regard and appreciate the clauses considered as bridling its use. I do not claim that the clauses mentioned are exhaustive in the sense that they will satisfy all needs, […]

        What clauses mentioned where? If he had been more specific we might have a better idea about what gotos he might allow, if any.

        He’s a bit more specific in his penultimate paragraph:

        I remember having read the explicit recommendation to restrict the use of the go to statement to alarm exits, but I have not been able to trace it; presumably, it has been made by C. A. R. Hoare. In [Algol W] Wirth and Hoare together make a remark in the same direction in motivating the case construction: “Like the conditional, it mirrors the dynamic structure of a program more clearly than go to statements and switches, and it eliminates the need for introducing a large number of labels in the program.”

        The case example is clearly not about longjmp, so he evidently approves of using structured statements instead of goto within a procedure. His “alarm exits” are less clear: does he mean break? goto fail? throw? Too vague to be sure.

        1. 2

          I think you meant inter-procedural (between different procedures) not intra-procedural (within the same procedure).

          D’oh! Yes. Typical of me to make an error that completely inverts what I’m trying to say.

          What clauses mentioned where?

          I believe these are if, else, case, and while/repeat, i.e. the other textual indices that largely render goto redundant (only really because those are the other things referred to as “clauses”).

          alarm exits

          This sounds like an exit() or panic!() type deal, or maybe a statement that puts the system in a global error state, but I suppose a programmer in 1968 would have a more specific idea.

          I guess I understand the point to be that intermingling (rather than fully nesting) textual indices (if/case/etc) and dynamic indices (basically, procedure definitions) is confusing, and inter-procedure goto is the only statement that can do that. Intra-procedure goto doesn’t, which is why I take the point to be about longjmp style goto.

          1. 1

            D’oh, I was confused by the way he used “bridled” which to me means restrained or limited somehow – if A is an improved alternative to B then I wouldn’t say A bridles B even if it’s linguistically cute.

      2. 1

        I always thought of it as “Unstructured goto Considered Harmful” because the big push at the time was structured programming and the key negative interaction between structured programming and goto is whether your function/procedure call can have more than one incoming edge.

        (i.e. Unstructured GOTO lets your function have more than one entry path, while C-style structured GOTO enforces that you cannot.)

    3. 4

      In summary, Dijkstra’s argument is really that goto’s lack of implicit semantics make its use challenging. I would argue this is the true thesis of his seminal essay. However the title obfuscates this as a thesis, as the Considered Harmful movement it inspired seems mostly concerned with discouraging a feature’s use by any means.

      It’s a bit unexpected, but the “considered harmful” title was not actually Dijkstra’s! He originally sent in the essay as “A case against the goto statement” and it was renamed by the editor Niklaus Wirth. While the original title also fails to convey this thesis, it does not do anything to obfuscate it.

      I’ll let him tell it:

      Finally a short story for the record. In 1968, the Communications of the ACM published a text of mine under the title “The goto statement considered harmful”, which in later years would be most frequently referenced, regrettably, however, often by authors who had seen no more of it than its title, which became a cornerstone of my fame by becoming a template: we would see all sorts of articles under the title “X considered harmful” for almost any X, including one titled “Dijkstra considered harmful”. But what had happened? I had submitted a paper under the title “A case against the goto statement”, which, in order to speed up its publication, the editor had changed into a “Letter to the Editor”, and in the process he had given it a new title of his own invention! The editor was Niklaus Wirth.

      Edsger W. Dijkstra

      (Transcribed by me from this handwritten note (last page) I found from Wikipedia)

      Instead it seems that there is a lesson about viral headlines and clickbait buried in this history. While Dijkstra’s essay was absolutely an attempt to alter the idioms of the day, I will reckon that without Wirth’s title it would have had far less impact on the goto statement’s decline.

    4. 4

      Rather, I argue that overly relying on best practices can prevent proper understanding of the underlying reasons from which they arose. This in turn hinders your learning. A nice corollary would be that you can easily learn the idiosyncrasies of a tool by studying the reasoning behind best practices.

      I agree with this. It is valuable for people to understand why patterns are used, as well as the downsides and upsides of alternatives. I heard Martin Fowler suggesting “sensible defaults” as an alternative to “best practices”, and I think that’s a more accurate description of what they should thought of as. It suggests that there are situations where they don’t apply, and that you should use the default unless you have good reason not to.