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
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;
}
}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) {
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.
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);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