Hacker News new | ask | show | jobs
by lmm 4703 days ago
Short circuit returns are the devil - they make it much harder to factor out part of a function into a smaller function. A function should have one entry point and one exit point; that's the whole point of structured programming. If you're going to return from some random point in the middle of your function you might as well be using goto.

(Of course, good programming languages provide a better solution than pyramid-of-doom nesting)

6 comments

> Short circuit returns are the devil

vi!

Naïve, absolutist positions in areas of long-standing debate between programmers of great experience and the highest imaginable competence just makes you look ridiculous.

Naive, absolutist positions in areas of long-standing consensus between programmers of great experience and the highest imaginable competence makes one look even more ridiculous.

By and large, the best programmers eschew nesting in favor of early returns. Invariably (in my experience) those who argue against early returns are inferior programmers (and not only by virtue of lacking taste in this particular debate).

Where did you get that idea of consensus? A lot of languages do not even have a return statement, neither does lambda calculus. Furthermore, CS community has long abandoned statement based languages in favor of expressions and relations which do not feature "return" for onvious reasons in forms other than equalent to jump.
I'm talking about programmers, not computer scientists. That is, people who actually accomplish things in the real world by hacking on software, rather than pontificating about it from their monadic ivory towers :) The latter have "abandoned statement-based languages," but the former absolutely have not.
I don't see that distinction. The are languages designed to map directly to register machines internal operation, like C or C++ or fortran and those are statement based because that's how the machine works. There are also high level languages, designed to express computation, and those are expression based, again, beacause computation is inherently based on expessions. There are other kinds of languages as well, for other purposes (relations, queries, etc).

Do you really belive that only people working on low level register transfer level things "accomplish things"? That's souds like a very old assembler argument.:-)

> By and large, the best programmers eschew nesting in favor of early returns.

Yup.

> Invariably (in my experience) those who argue against early returns are inferior programmers (and not only by virtue of lacking taste in this particular debate).

Yup.

> Short circuit returns are the devil

No, they're not.

> they make it much harder to factor out part of a function into a smaller function

They generally make the function smaller, that's what guard clauses are for.

> A function should have one entry point and one exit point

That's absurd and makes for very ugly and unnecessary code.

> They generally make the function smaller, that's what guard clauses are for.

My point is: frequently, one wants to extract a part from the middle of a function to make a new function. This is very easy (and can usually be done automatically) unless said part contains a return.

Suffering the unbearable burden of a single exit point just so your refactoring tool has an easier time breaking up the code isn't a trade worth making, especially when it's the single exit point that's probably making it too long to begin with.
> A function should have one entry point and one exit point; that's the whole point of structured programming.

[citation needed]

> A function should have one entry point and one exit point; that's the whole point of structured programming.

I think a reasonable primary source for "the whole point of structured programming" is Dijkstra's "Go To Statement Considered Harmful"[1]. It's a short article and once you get past his writing style, his point is simple and lucid.

In the first part he lays out a simple question: given a program text, how much information do you need to track of to correctly identify where the program is currently executing at some point in time and how it got there? Roughly, if you were paused in the debugger, how much state does the debugger have to hold in order to be able to resume where it left off?

He's asking this because the smaller amount of state required for this, the easier it is for a human to look at a program text and figure out what can happen while it's running dynamically.

If all your language had was assignment statements, it's simple: you basically just need a single line number. Adding "if" and "switch" for branching doesn't add any more complexity. And, of course, reading code like this is pretty trivial.

If you have procedure calls (and by implication, recursion), you need a stack of those numbers, which is exactly what the callstack in your debugger holds.

When you add "while" and "for" for looping, you need to keep track of how times you've gone around the loop.

Now, if you add "go to" everything goes to hell. If you're on line 10, did you get there because you were on line 9 before, or because you jumped to it, or some random combination of those? It's a total mess.

Then he says:

> I do not claim that the clauses mentioned are exhaustive in the sense that they will satisfy all needs, but whatever clauses are suggested (e.g. abortion clauses) they should satisfy the requirement that a programmer independent coordinate system can be maintained to describe the process in a helpful and manageable way.

Here he's saying you can add any other "clauses" (flow control constructs) to a language that you want as long as you don't add to the amount of state you need to store to keep track of how you got there.

For example, adding an "unless" statement that works like "if" but only has an "else" clause instead of a "then" clause is peachy. You don't need any additional data to keep track of where you are.

You know what else doesn't require adding any additional data? Early returns.

So as far as Dijkstra is concerned, no, avoiding early returns is not at all the point of structured programming.

[1]: http://www.u.arizona.edu/~rubinson/copyright_violations/Go_T...

Having trouble picturing a better solution that isn't also morally equivalent to goto. Would you mind providing an example of your preferred approach?
Straight exceptions are also goto-like, but there's the slight improvement that they allow you to (automatically) refactor out the middle of a function without changing the behaviour.

The solution I prefer is a monad with some light notation (Haskell do, scala for/yield). So it looks like:

    def doSomething(): Validation[Whatever] =
      for {
        result1 <- callThatCanFail()
        result2 <- callThatCanFail2(result1, 4)
        intermediate = callThatCantFail()
        result3 <- anotherCallThatCanFail(result2 + 4, "Hello", intermediate)
      } yield result3 + result1
The <- syntax clues you in that this computation is going on in some kind of "context" - if you're working with futures it might happen on a different thread, if you're working with collections you're iterating over the elements. Here we're working with a validation, and our computations only happen if the previous one succeeds.

Is this goto-like? You could argue so - once one computation fails, the rest turn into noops and we pass immediately to the end. But to my mind this is like replacing an if/else with polymorphism - our validation context is an object with certain behaviours, and we're calling methods on it with our functions as parameters, which will behave differently depending on which subtype our context instance is.

I don't see how the <- clues you in any better than the error handling that things might exit early. It seems from your response that the goto-ness of the go solution is not what you're really objecting to, because your proposal is equally goto-y. As far as I can tell, it's really the if-else verbosity that you don't like, which is fine (and I agree, exceptions are better), but it has nothing to do with the "whole point of structured programming." And, unless I'm mistaken, structured programming also lacked exceptions in its initial formulation.
I think the main thing I'm objecting to is the possibility of multiple paths through the function. Doing it this way there's only one possible flow: a series of calls that pass blocks to validation objects (that then may or may not execute them).

Compare how smalltalk didn't have a conditional control flow statement. Rather, the boolean type has a polymorphic method that takes a block and then executes it or not.

The other nice thing about this approach is it makes the language simpler and more regular because it's implemented using standard language constructs rather than a special statement. E.g. you can write a "gather" function that takes a collection of monads and returns a monad of the collection, and this is generic in the monad (so the same function works to turn a List of Futures into a Future of a List, a List of Validations into a Validation of a List, etc.)

No, no, no. That's what college professors with no real world experience tell you. In the real world, professionals use guard clauses to exit early all the time.