Pair it with naming your complex and chained expressions and suddenly you have some seriously readable code.
So far, I have never seen a valid scenario where a switch statement is actually any better than if.
Pair it with naming your complex and chained expressions and suddenly you have some seriously readable code.
So far, I have never seen a valid scenario where a switch statement is actually any better than if.
In typed languages a lot of those checks get implemented in your actual types, but you still might have various business logic/data integrity checks you might implement in early return.
Seen in this light, this pattern's really not so much in tension with the idea of having a single return variable. It's just a way to implement the idea that invalid states should not be possible, which you accomplish in your type system when and if possible, and fall back to runtime checks for the gaps where it's not.
That said, I much prefer early return whenever it makes sense. In functions that do have a lot to unwind and many possible points of failure I'll pull out the old villain 'goto' and have the unwind code at the bottom of the function.
Strictly sticking to only one approach is usually a mistake. One that is repeated a lot in computer science. There are schools of thought that if everything is the same it will be easier to understand, but what happens is problems that don't exactly fit the mold end up being solved in awkward and inefficient ways. Or development gets slowed because you have to refactor your problem around the tools instead of the other way around.
This is where Lisp's unwind-protect, finally blocks in some languages, defer in Go, and C++'s RAII pattern can come in handy. Especially if you have a healthy respect for goto and want to minimize its presence in your code for various reasons.
`defer`, on the other hand, is probably the only thing I wanted to take from Go to other languages I worked with. Beautifully explicit and wonderfully useful.
Old:
using (var file = OpenFile())
{
// use file
}
New: using var file = OpenFile();
// file will be disposed when it goes out of scopeIt would be cool if someone made a way to scope variables at the function level in JavaScript instead of at the block level. I might write a transpiler for it…
https://github.com/tc39/ecma262/blob/master/CONTRIBUTING.md
So, impostor syndrome aside, why not? Wanna team up on that?
It would be nice if there were an equivalent in the native API. You can use .finally() with chained promises, but as far as I know there's nothing comparable with async/await yet, and that's a much more comfortable syntax with which to work with promises overall.
function foo(x) {
if (x == null) {
return null;
}
x += 2;
return x;
}
I can't just look at "x += 2;" and know whether or not it's conditionalized. I have to have the full context including the early-return, and then reason about the control flow from there. Whereas: function foo(x) {
if (x == null) {
return null;
} else {
x += 2;
return x;
}
}
Here I can tell just from the else-block that this is one possibility which will execute if the other one does not, and vice-versa. Their indentation is the same, cementing their relationship. If I want to know whether this block will execute I need only look at the if()'s, not their contents.return nil if n.nil?
I asked on the go-nuts mailing list about perhaps including them in go2, but it seems many of the people who replied hate them.
If people tend to write:
function foo(x) {
if (x == null) {
return null;
}
// do some stuff to the code
for(a reason) {
// do some more stuff
if(some detailed reason) {
return null;
}
// more things
return 7;
}
Then the shape of the code and the "pattern" of early returns gets broken.If you read code where all of the short-circuit, early-return logic is at the start of the function, and then all of the work is done, do you still have this opinion?
E.g.
function foo(x) {
if(x == 1) { return null; }
if(x == 2) { return null; }
if(x > 7) { return null; }
// do things with x
return 7;
}
? (cond
(early-return-condition early-value)
(second-early-condition early-value2)
...
(t final-else-value))
The "default case" is on the same footing as the other cases. Only in procedural languages does the control-flow get confusing if you aren't careful.Also - at the very least - you can mimick the "flat" early-return style and just stick some else's in there and call it a day. That's my main point; less so the actual deeper nesting
function check(x) {
if (!test1 (x)) return false;
if (!test2 (x)) return false;
if (!test3 (x)) return false;
if (!test4 (x)) return false;
return true;
}
In _this_ scenario, it is true that this is much more readable than: function check(x) {
if (!test1 (x)) {
return false;
} else if (!test2 (x)) {
return false;
} else if (!test3 (x)) {
return false;
} else if (!test4 (x)) {
return false;
} else {
return true;
}
} function check(x) {
if (!test1 (x)) return false;
else if (!test2 (x)) return false;
else if (!test3 (x)) return false;
else if (!test4 (x)) return false;
else return true;
}
I think this is more clear than either of the aboveTrue. I still think the first is clearer though. Either all tests pass, or they don't.
Your first example would still look clean like this:
function check(x) {
if (!test1 (x)) { return false; }
if (!test2 (x)) { return false; }
if (!test3 (x)) { return false; }
if (!test4 (x)) { return false; }
return true;
}
And it would be much safer from stupid copy/paste errors. function check(x) {
if (!test1 (x)) return false;
if (!test2 (x)) return false;
if (!test3 (x)) return false;
if (!test4 (x)) return false;
return true;
}
why wouldn't you do something along the lines of (every (lambda (f) (funcall f x)) (list #'test1 #'test2 #'test3 #'test4a))
or whatever is the equivalent in your preferred language? if (user == null) return false;
if (IsCompleted(user)) return true;
if (action == null) return false;
if (value < 0 || value > 100) return false;
You're not usually just passing a single value to a bunch of "test" functions. function check(x) {
return test1(x) && test2(x) && test3(x) && test4(x);
}
Or, since I assume is JavaScript, just: const check = x => test1(x) && test2(x) && test3(x) && test4(x);That is, I don't think it matters too much whether you do:
function div(x,y) {
if (y == null) {
return null;
}
if (y == 0) {
return null;
}
return x/y;
}
or function div(x,y) {
if (y == null) {
return null;
} else if (y == 0) {
return null;
} else {
return x/y;
}
}
as they both accomplish the same major benefit of the pattern.The real thing it's helping to avoid is accidentally creating something like this:
function div(x,y) {
if (y != null) {
if (y != 0) {
return x/y;
} else {
return null;
}
} else {
return null;
}
}
which can quickly get very hard to reason about without in more complex cases.In practice, in C-like languages, I tend to see the "else-less" version which is why I brought it up
I put all my early returns at the top as these are the preconditions for the rest of the code; I don't think that's hard to understand.
const foo = x => x !== null ? x + 2 : null
I much preferred (and still greatly prefer in industry) the pattern of exiting the function as soon as possible.
Returning and throwing quickly and strict.
int foo(thing_t \*work) {
int ret = E_SUCCESS;
if(work->thing == BAD_THING) {
ret = E_BAD;
goto exit;
}
do_some(work);
if(work->thing2 == OTHER_BAD_THING) {
LOG("whoopsies");
ret = E_OTHER;
goto exit;
}
ret = do_more(work);
if (!ret) {
LOG("lazy");
goto exit;
}
ret = last_bit_of(work);
exit:
return ret;
}Not sure it makes much sense to indent everything within "if" and if you forget to "else", you've just potentially hidden a bug.