Early exit is a tail call optimization of procedural languages
enterprisecraftsmanship.com
enterprisecraftsmanship.com
Right now? I'm early return.
When I wrote C and C++ code with locks and malloc/free? One exit only.
It's all about avoiding bugs.
In C code heavy with locks and memory allocation, you need to make sure your post-conditions are correct. That is near impossible when there is more than one return [1].
In C++, with RAII and Java with "try-with-resources", I can ensure proper cleanup. At that point, it's about making it easy for someone who is reading the code to avoid screwing it up. I need to make it easy for them to flow through the code, keeping the cognitive load as low as possible. Functions do what they say on the box and _only_ what they say on the box. At that point, my goal is to make a decision, act on it and move on. I don't want to carry that state forward - it means I have to keep it in mind for every line after it. I'm even trending towards having the majority of the methods in my classes be static functions with no state.
Finally, if I've got a function that has two different flows and I'm deciding between them with an if? I've probably actually got two different functions pretending to be one. I then try to refactor and get rid of the if.
[1] http://programmers.stackexchange.com/questions/154974/is-thi...
• Early returns vs. single return.
• Excessive nesting vs. simpler flatter if/else.
The real problem with the "before" example is the excessive nesting. I do prefer early returns myself, but even if you wanted to use the single-return style, you could simply un-nest the whole thing:
public string Execute( int integer, string str, bool boolean ) {
string result;
if( integer <= 42 ) {
result = "Error: Integer is too small";
} else if( string.IsNullOrWhiteSpace(str) ) {
result = "Error: String is null or empty";
} else if( ! boolean ) {
result = "Error: Incorrect boolean";
} else {
result = "Success";
}
return result;
}
It's a bit ironic that this is C# code, because ReSharper would offer to un-nest the code for you.Also, this really has nothing to do with tail call optimization.
if (x == null)
return "x is required";
if (x.length < 3)
return "too few elements in x";
if (x[0] == null)
return "first element missing";
The point here being that each successive check is testing something that is only valid if the previous check passed. This can't be so easily converted to your if/else pattern without re-encoding the early exit in the form of a complex boolean (&& and || have early exit built in).The concept's applicability to tail call optimization is only by analogy with the mental stack; just as tail call optimization sets up the call such that the current stack frame no longer exists, early exit sets you up to remainder the rest of the code without regard to any potential alternate code paths (i.e. a potential else branch on an early if statement) later; the mental frame of a long if statement no longer exists. The analogy is a little bit stretched; of course it is not a technical isomorphism, but nobody ever said it was.
My only point in showing that code was to illustrate what I see as the bigger problem in the example: the use of nested if and else statements when non-nested ones could be used instead. Personally I would get rid of the nesting by using early returns, but for someone who objects to that for any reason, the non-nested if/else chain is a nice alternative.
Also, the series of returns you show here could easily be written as a chain of if/else for those who prefer that style. I'm not sure why you said it requires a complex boolean expression; this code is equivalent to the series of returns:
string result;
if( x == null ) {
result = "x is required";
} else if( x.length < 3 ) {
result = "too few elements in x";
} else if( x[0] == null ) {
result = "first element missing";
} else {
result = "the real result";
}
return result;
Again, I don't particularly like this style, although it is better than the deeply nested if/else. I prefer the series of returns that you listed in your comment. (define (example x)
(call/cc (lambda (return)
(when (< x 0) (return #f))
; more code, including possible more calls to return
0)))
However, most functional languages do not have callCC built in. Happily we can get a similar effect by writing our code in continuation-passing style (CPS)[2]. (Think Node.js.)Nominally, CPS has some overhead because of the extra function calls involved. However, a CPS transform also puts every call in tail position, making them eligible for tail call elimination. This means that we can use CPS with no function call overhead in a language with proper tail calls.
What does this mean in practice? It means that calling a callback in CPS style—which, being in tail position, can be eliminated—is effectively just a jump under the hood. Not too different from return!
Putting it all together, we get that tail call elimination lets us write code in continuation passing style with no extra over head which gives us access to continuations that we can use to implement an early return in languages without return.
This turns out to not be what the article was on about, but I think it's more interesting.
[1]: Code from a StackOverflow answer by Nathan Shively-Sanders which goes into more detail: http://stackoverflow.com/questions/2434294/scheme-early-shor...
[2]: I wrote a brief explanation of CPS on Quora: https://www.quora.com/What-is-continuation-passing-style-in-...
One way to think about it is that languages with callCC built in are just transformed to CPS by the compiler before being run, which includes transforming the definition of map. (In fact, this is a reasonable strategy for compiling Scheme, I believe.)
It's pretty easy in Haskell which wraps CPS into a monad (Cont) because you use mapM from the Control.Monad library instead of having to write a specifically CPSed version of the function.
Sometimes it is required. In Java, before try-with-resources, if you had any resource management (locks, file descriptors, sockets), a single exit point made resource management simpler. C code tended to have similar rules (or GOTO's to jump to the exit block in the function). C++ just used RAII, and made it the destructor's problem.
It is pointless in languages like C# or Java which have GC and 'try' or 'using' blocks. In some cases the single return is nicer, but this is not universal nor is single return "required" in all cases.
In functional languages such as F#, Haskell or Erlang where the method is usually side-effect free and uses pattern-matching, multiple return-points are completely normal and no-one even sees an issue.
Resource resource = new Resource();
try {
if (a)
throw new Exception("Oops!");
else if (b)
return resource.doBStuff();
else
return resource.doStuff();
} finally {
resource.close();
}For example:
public string Execute(int integer, string str, bool boolean) {
string result;
if (isValidInteger) {
result = validateStringAndBool(str, boolean);
} else {
result = “Error: Integer is too small”;
}
return result;
} private string validateStringAndBool(string str, bool boolean) {
if (!string.IsNullOrWhiteSpace(str)) {
return isValidBoolean(boolean)
} else {
return “Error: String is null or empty”;
}
}
private string isValidBoolean(bool boolean) {
if (boolean) {
return “Success”;
} else {
return “Error: Incorrect Boolean”;
}
}
And yeah, I can't see how this has anything to do with tail call optimization other than a fluffy analogy to stacks - mental and programmatic.https://github.com/BartoszMilewski/Okasaki/issues/1#issuecom...
his blog has more details
https://github.com/BartoszMilewski/Okasaki/issues/1#issuecom...
This guy knows what he is talking about. His blog has a lot of interesting discussion in this area.
Having multiple return statements in a method actually renders the method LESS readable. I understand the first proposal is also usually implemented with many return statements, but they could be replaced with a variable assignment, variable that is then returned at the end of the method (as in the example). If that variable is final (or whatever the C# equivalent is), that's even better.
I 100% disagree. Introducing a variable instead of using multiple returns means I now have to keep track of yet another variable throughout the entire rest of the method. Is it modified before it's returned? Gotta read the whole thing to find out.
Early returns reduce the complexity of a method by reducing the number of possible states as you move through the method.
As I said in other comments, early returns for exceptions and bad input are fine. They are exceptions, though, not return statements. Go ahead and use return if you want/can, although I wouldn't.
If it's not for an exception or input validation failure, then we have something like:
"Alright, so here we do this, unless this, that and that other condition over there were true earlier, because the method would've returned then..."
I find that the complexity to keep track of with this approach is pretty much the same, or even more depending on the branching.
I think early returns deep in the logic of a method can be problematic, but I think early returns at the top of a method before you get to the meat of the logic are very readable.
A sufficiently complex method with 5 return statements all scattered in between to treat real cases (i.e. not failures or conditions that should raise an exception) is certainly less readable than the same functionality refactored to a single return statement at the end with the proper use of branching.
Aside from readability, it also increases maintainability. What if you have to introduce a modification to your logic that applies a transformation to the result? Should you modify it in 5 places, or 1?
In all cases? How can you even measure that? How can you do it in a way that's free of bias?
If you've never used a style, of course you will find it harder. Dogmatic assertions that boil down to "what I'm used to is objectively better because of how it feels to me" are IMHO harmful.
I've read (and sadly written) enough code in both styles to know what _I_ (and all the peers I've bothered to ask with concrete examples, for that matter) find easier to understand, but there's of course all kinds of bias in there as well.
Taking comments in HN as fact or truth is harmful, and (replying to your other comment) waving acronyms around is equally harmful, if not more. You can't possibly assert that writing code in one or another way is easier or harder, or that the code is "more complicated" (precisely what we don't seem to agree on). To be honest, I find it easier to write methods with single return statements (and exceptions to handle... well, exceptions[^1]). It all seems clearer in a "stream of thought" kind of way. Engrish is hard, sorry.
[^1]: Early returns are exceptions for lazy coders.
In general, of course not. I think that both single-return and multiple-return styles can be valid and readable. But of course both can be obfuscated.
But give me a particular piece of logic and I might give an opinion on which style I would use for it.
& if you think YAGNI is harmful, you should see the pointless code from people who have never thought that way!
Lines of code and cyclomatic complexity (2) is just the start.
You would have to do something like ... e.g. take a group people skilled in some other programming language, give them a basic grounding in Javascript, and have them work with some JS code samples with and without nested callbacks.
If there is clear difference in code comprehension, time to modify the code, lines of new code to accomplish a goal, and resulting bug count then you can have an informed opinion on if this code style is really objectively so bad.
That seems like a costly and time-consuming procedure. But if some academic institution did this I'd be pleased - some scarce data and some actual science is IMHO is worth a thousand opinions in this kind of recurring stylistic debate.
In 1 place, clearly. The multiple-return version would require a bit more refactoring at this point, either extracting another method or introducing the "return value" state var at that point.
Why not code it with single return up front? Simple: most likely, You Aren't Going To Need It. The YAGNI principle says to make the code more complicated when you need to, not before.
With a single return, I can at least follow that thread fairly easily and sometimes it's just a few lines back in a large function. With many returns, I'm forced to start at the beginning of the function.
There a dozen or so other culprits that make this difficult. Assignment in a block can be just as bad, for example, since I can't know which assignment occured without looking at all of them.
The function signature should be explicit about what it does and returns. If it has a static/defined return type I know what I can expect from it. The function name should also be explicit about what it does and returns.
Scrolling down to the end just to see a 'return books' statement is usually useless.
If I need to read the whole function, I'd rather have early exits instead of nested ifs. It's easier to read because it follows the logic of "if (such and such condition): nothing else to do -> return;"
Large functions that have both side effects and return values are themself the readability problem.
Early return is not, it is a succinct way to express some functions with return values.
I will admit though that no programming style will sufficiently protect against bad coding :). I'm sure even if this person did everything I advised, they'd still find a way to overcomplicate and confuse things. So maybe the issue is moot, or at least bigger than style.
guard let something = maybeSomething else { return ... }Does it? I don't find it that. However, if you have never read code of this style before, it will be unfamiliar to you and you will find it less readable. But you can overcome this with practice.
I would rather say that it depends - both are valid styles, sometimes it's more readable to use multiple return statements and sometime less. As a programmer you know both and pick the option that is better suited to the problem at hand.
On the other hand http://www-cs.stanford.edu/people/eroberts/papers/SIGCSE-199... offers concrete reasons why early returns make sense.