Early returns is not a feature of the language. It's an artefact of assembly language. You have no reasons to use it in the first place.
In most cases, I think it is perfectly fine to have early exits on parameter checks at the start of the function/method.
I believe you'd be surprised what aberrations you'd find in your code and all the simplifications you could make if you would removed all the jumps.
I also write a lot of JS and tend to use guard clauses fairly liberally. I find they make boundaries (x < 0) much clearer. My functions are also < 10 lines in most cases. For me it leads to less convoluted code but YMMV.
Other programming languages let you return from any line in the function, no matter how deep in a conditional, inside nested loops, before or after variable side effects, etc. Tricky to know just _what_ that function will do given some inputs.
def transform_data(raw_data) when length(raw_data) == 1, do: []
def transform_data(raw_data) do
# actual function code goes here
end
That said, I wouldn't want to see the above snippet in my team's codebase :) def transform_data([_|[]]) do: []
I just wanted an example of using guard clauses. It does depend on the actual code though, there's probably a more elixiry solution.I suppose I could get rid of early returns with a variable and breaks, but is it worth it? My functions tend to be small enough for me to not run into problems with early returns.
They are literally goto end. It used to be done like that a lot actually.
I suppose I could get rid of early returns with a variable and breaks, but is it worth it?
Very good question. My claim is that yes it is tremendously worth it if you care about great code. The reason is because by introducing intermediate variables (or by using more the ternary operator ?) you will have a more expressive code, and then you will subsequently take better decisions when you will write/maintain/refactor it. And more importantly, you will refactor it because you can (how do you move code that's full of early returns?). At the end, your program will be much simpler and better (less bugs, faster, etc). Just try it in a big-enough program, you will not come back to the old ways, I think.
My functions tend to be small enough for me to not run into problems with early returns.
Very interesting. This point would require a live discussion. Many things are at play here, between over engineered object-oriented programs where everything is broken into little pieces that shouldn't (most of the time you should make a function when it's called twice, not because it represent a world object) and also, I'm not surprised you guys don't do bigger functions since you fill them with early returns. In clear, what I'm saying is: are you guys not able to write any big functions because of the early returns? I wasn't expecting that, sounds almost plausible. You end up breaking things up way too much, over engineering things and all just to avoid bigger functions because you can't have them because of the early returns? Very very interesting. Of course this is complete conjecture and certainly not true. Or is it?
edit: it also depend on what you mean by worth it. In terms of market value, you will not be rewarded because the market the doesn't reward great code so much anyway, broadly speaking.
So are a lot of other constructs:
break is goto the end of the loop, continue is goto the beginning of the loop, the closing of a loop block is goto the beginning of loop, else is goto the end of the conditional, if is maybe goto the next else, and return (early or not) is pop from the stack and goto that address.
The fact that a language feature uses a JUMP instruction does not make it necessarily evil; most of them do. Everything has both benefits and drawbacks which are often highly situational.
You may find it easier to convince people of the problem with early returns if you limit the scope of the argument to a more specific context than “always”: it’s usually easier to make an argument for a concrete case rather than an abstraction, and then show how the same reasoning applies to a large class of situations.
Do I understand correctly that you tend to make large functions because they're easier to refactor (and not returning early makes this easier)?
Option<T> sink(int pos) {
if(pos>=this.length()){
return Nope;
}
// code that has depends on the bounds check
Some(value)
}
Versus: Option<T> sink(int pos){
Option<T> ret = Nope
if(pos < this.length()) {
// same code
ret = Some(value)
}
return ret;
}
I argue that the second is more complex: an added variable, deeper nesting, useless return line.When rewriting multiple guards you get more empty statements that reassign the default value of the return.
I do have the following opposite opinion too: guards should be simple: no loops, no complex control flow (name the loop). No side effects in guards. And no guards handled by the expected case, even for clarification.
return pos < this.length() ? Some(value) : Nope
See, mine is already way more simple than yours. This is my point, if you force yourself not to use jumps, you will end up with simpler and better programs. Also the second example you wrote is expressive and more importantly you can move that code. Also note that in many other programming language like lisp and haskell, you would never need the intermediate variable as you can basically write return if...And even then I would reverse the order, placing the exceptional and short case first.
Moving the code: why? You have a perfectly encapsulated function. If you need to, you have more problems with shadowing variables, functions and with return types.
For functional languages: doesn't Haskell explicitly have guard pattern? I thought the general guidelines favored matching and guards over if statements.
LOL. Try adding returns, continues, and breaks where appropriate to some random code that doesn’t currently use them, and you’ll see what wonderful things happen. It’s literally magic. The control flow becomes clearer and more linear and the code becomes more logical and concise, with fewer weird conditionals repeated several times for no reason. Also often improves performance, “for free”.
It seems we have different experiences because where I see early returns used is mostly where functions are already small.
You can certainly over-engineer things just to avoid bigger functions. However from my experience I see that far less than complicated large functions with little care to readability.
Good question. This is true, but this is a programming language bug. In some programming language you can write the equivalent of return if... or return for.... Also note that those intermediate variables add a lot in expressiveness (you're computing result or screensize etc). The mutation is unfortunate indeed but a programming language bug.
But lately I've been working with Elixir which doesn't support early returns and while Elixir does have some tricks up its sleeve to better handle the idea of having an early return without using it (such as pattern matching on function arguments and / or guards), I'm now slowly shifting into thinking early returns in any language makes things more difficult to reason about in the end most of the time.
I say "most of the time" because for very short functions where you might return early to avoid an else clause or deal with varying types of input, that's not too bad. It's really a problem in the use case you explained where you have a larger function that might have 4 or 5 early returns scattered about and that function might be nothing more than having a few short if statements that say "if this, then do_something and return". It becomes difficult to maintain because now the function ends in 5 different spots based on 5 different conditions.
return rawData && rawData.length > 1 ? rawData.map((item) => item) : []
In many other cases, I would use a result variable. It's more expressive and more importantly you can move that code (the result variable may become just another variable in a bigger function). (note that in many programming language like lisp or haskell you wouldn't need the result variable because everything is an expression) Granted, it looks like no big deal in many many spots. Until it is, particularly at scale when you can't refactor anything because everything is full of early returns.These small concrete examples are good to talk about. How would you rewrite the following:
def contains_3(xs):
"""Returns True if the given list contains a 3."""
for x in xs:
if x == 3:
return True
return False
It does use an early return, but it is not long or convoluted. You could use something like `any()`, but I would never find fault with the above function or attempt to refactor it. It is as clear and efficient as possible, and uses basic language features even beginners would be familiar with. def contains_3(xs):
"""Returns True if the given list contains a 3."""
found = false
for x in xs && !found:
found = x == 3
return found
Not a valid python code but who cares. Using proper python, you could write the whole thing in one line using list comprehension.In terms of expressiveness (as much as this short example needs expressiveness), note how found is x == 3 which is what you want right. Note how the loop condition is now full. One could also note how every line is about found so you can instantly tell what lines is about found, which one is about foo or bar, if you'd picture a bigger function. Note how there is now only one condition (the loop) and not two (loop and if) (in many situations you would end up with less lines of code, surprisingly). Finally, note how DRY this is: you return x == 3, not True if x == 3 is True (there is many cases where this DRY property is more clearly visible).
This code reads like a list comprehension.
In addition to be more expressive (although, again, this is more striking on a bigger function) and movable, you can also actually write bigger functions like that which you can't with early returns (everybody agrees this is a mess).
If I'd tell you not to use jumps, you would have found that solution. Simply ban jumps from your code (find the alternative way of writing things) and you will obtain a code that will surprises you, especially in bigger functions.
I don't think so, even after seeing your pseudo code don't see a nice way to avoid the early return. I don't understand how your for-loop / if-condition mix works, but let me try to translate it as best I can into valid Python:
def contains_3(xs):
found = False
index = 0
if not found:
found = xs[index] == 3
index += 1
return found
This was honestly my first best effort attempt at creating this function without an early return. But it is wrong, it only checks if the first value of the list is 3. My opinion is that this style is more error prone (and indeed, I did create an error), and objectively there is more code and more state and thus more opportunities to get things wrong. def contains_3(xs):
found = False
index = 0
while not found:
found = xs[index] == 3
index += 1
return found
That works, but again, there is more code and more state. When reading this code I must ask myself "are they iterating over the list correctly?" and I must spend mental effort answering that question. This doesn't happen with a for-loop.I don't understand why you keep talking about jumps in assembly code. Code which has zero early returns will still have hundreds of jumps in the assembly code.
Code that is short is good. Code that is readable is good. I don't think we'll agree on what code is more readable, but we can at least agree on which code is shorter.
As I've gained programming experience I care less and less about how individual functions are implemented and more and more about what those functions do and what they return. For something like `contains_3`, I see that it is a function with no side-effects, which returns a boolean, and wont fail (at least, wont fail in a language with type checking). This is a small simple function, and I don't care how it's implemented, it is a good function. If it becomes a problem for some reason, it is isolated and can be easily fixed in isolation.
Also beware of people with strong opinions about way too general things in areas where specific things are.. specific, and where discussions of the sort you started are nearly almost a bad sign. Without something concrete to solve/look at these discussions are more religious in nature. You can say whatever, no way to prove or, worse, disprove. When people get into heated discussions there is a good chance that there is no practical test possible because the discussion is not about anything concrete but about "ideas" - where everybody has different ones for the same keyword(s).
And how would putting these early returns in a gigantic if condition or several increasingly indented if blocks fix these bugs? It will not fix any bugs since both forms are equivalent.
>early returns are gotos
All types of control flow can be represented with goto.
>gotos are harder to follow
Harder to follow than what? The majority of programmers are familiar with following gotos by using an abstraction over it such as using loops or if blocks.
>You can't move easily (think copy/paste) code than has early returns
If code is able to be moved to or be copied into another part of the codebase, this is a signal that the piece of code should be put in a function. If you are talking about using a subset of code following early returns, then you do run into the problem in not knowing what subset of early returns (aka preconditions) that are needed to bring be in a new function. This is the same with using large or nested if blocks.
>Early returns are artifacts of the assembly jump instruction
By this logic all control flow constructs are artifacts of that. Regardless whether or not you use early returns, you get equivalent assembly code after compilation.
>it makes no sense to have jumps in [a high level] language
I disagree. It makes perfect sense to have jumps in a high level language. A high level language should allow you to write abstractions over these jumps though. This way you can name and reuse patterns of jumps in your program. In addition you can think of early returns as a high level construct too instead of a low level one. You can think of if blocks as a monad. If we take a look at the article and used nested if blocks you could think of the type of the function as If If List (we can imagine that there is an interpreter behind the scene to convert this into a plain List). Since If is a monad it means we can do the join operation. This would turn it into a single If List. The obvious way to do this transformation by hand to the source file it would be equivalent to merging both if statements into a single one. If the else statements would return different results you would need to make sure you return the value associated with how far into the combined condition you get. Doing it this way is a little awkward. Also remember that monads give us the bind operation too. If we manually transform the source into using the bind operation we actually get the same thing as using early returns. Using early returns is a much clearer implementation of handling the conceptional If monad. By using the If monad we are only having to deal with a single if statement / condition at a time.
>Things becomes so much more expressive
I beg to differ. They are equally expressive, but early returns allow you to mention the preconditions / special cases all at the beginning of your function.
>Early returns is one of the big pillars of bad inexpressive entangled convoluted programming
I'm not sure how early returns make something entangled. Since they can only be at the beginning of a function, they can't really be entangled in your whole function.
>And what is people reason for using early returns? It makes less indentations
Yes, this is a side effect of being a monad. It means we can focus on a single if statement at a time instead of focusing on a bunch of nested if statements at the same time. This is the same thing as when javascript developers realize that converting callbacks that call callbacks, etc into using promises reduces the nesting that is needed.
In summary, I am not convinced from your argument that early returns are bad since they are an abstraction over assembly.
First the practical, in my experience, a big function with many early returns has a bug or has a convoluted logic, 100% of the time. I've literally made a living and a career by untangling big functions with early returns. I've really saved many situations and code just by doing that. The reason this happens is that early returns are gotos and gotos are harder to follow.
Also, you can't move easily (think copy/paste) code than has early returns, which means that the code is defacto harder to refactor. So in practice, code with early returns stays in place and grows.
Early returns are artefacts of the assembly jump instruction. They shouldn't even be in a high-level programming language because it makes no sense to have jumps in such a language. They make the programmer lives in the assembly language world (succession of instructions that update a state) instead of the wonderful world of high-level programming (expressions).
If you interested by what I'm saying, you should try removing all returns, continues and breaks from your code, you'll see what wonderful things happens. It's literally magic. Things becomes so much more expressive without them. Which is the point, high level programming is about gaining more expressiveness over assembly code.
Early returns is one of the big pillars of bad inexpressive entangled convoluted programming. And what is people reason for using early returns? It makes less indentations...
You may wonder why early returns doesn't sound that much like a big deal after all. And you are right, there is no big deal. No big deal in terms of what the market reward. Now if you care about the art of programming, this is a huge deal imo.
edit: thank you for the fair discussion. I would like to insist that obviously, if your code is more expressive, you will take better decisions and at the end, what I'm saying is that you will end up with a totally different program. Early return or not is not about cosmetic. It's about writing better program.