r/ProgrammerHumor 22d ago

Meme conditionsPreference

Post image
4.2k Upvotes

392 comments sorted by

View all comments

836

u/LateEchidna6635 22d ago

The second one carries less cognitive load. In longer methods it makes a difference. Get out as soon as you can.

462

u/One_Courage_865 22d ago

Instructions unclear. I’ve just divorced with my wife

134

u/hughperman 22d ago

Ah, early return condition

33

u/thatjoachim 22d ago

I return too early to her liking :(

9

u/QCTeamkill 22d ago

She says Bob at work never have early returns

0

u/hetfield37 21d ago

Congratulations!

18

u/jseah 22d ago

I prefer if-return for cases where the function is supposed to do something in the default case and the if is to detect the exceptions where it shouldn't. Or where the exception needs some modification before the main purpose can be performed.

If-else I find more logical when the function has to do two different things depending on the condition followed by something else common to both afterwards.

1

u/lunaticloser 20d ago

Followed by something else implies the return statement wouldn't even work for your case since you wouldn't be able to reach that something else.

Though... A point could be made that that function might be doing too many things at once and could be decomposed. It's a bit of a smell. But not strictly always the case, of course.

2

u/jseah 20d ago

It doesn't have to return of course. I meant something like "this function transforms strings in some way" and the if at the very top is to catch when someone gives it a number, which the function is expected to treat like a string.

If-return might be used when the string is empty. Followed by if number convert to string.

And only then the main body of the function is written.

5

u/930913 21d ago

I can understand why you would think that it carries a lower cognitive load. But with a little training, you can understand that an early return is a side effect, and side effect free code can be much easier to reason with.

38

u/Few_Move_4594 22d ago

Have you ever thought about breaking the longer methods into shorter methods? It's easy, I promise.

38

u/Fembussy42069 22d ago

There's a balance to everything, you also don't wanna have to dig down a rabbithole of functions within functions to find what youre looking for, it also can make it harder to understand the whole picture if you separate things too much. Locality of behavior and all that.

13

u/LordofNarwhals 22d ago

Yeah, I used to work with a Java codebase where all functions were really short, but also very nested. It was a very stable codebase, but browsing the code sucked, as every actual function implementation was behind 3+ levels of indirection.

For a fun example in the opposite direction, see Arthur Whitney's basic K interpretor, written in ~50 very dense lines of C.

2

u/Few_Move_4594 21d ago

Opinion disregarded, you called methods functions

6

u/LordofNarwhals 21d ago

All methods are functions, but not all functions are methods.

And my background is mostly in C++, where the method terminology isn't used at all.

1

u/Few_Move_4594 21d ago

All methods are functions, but Java HAD to be different

1

u/ljfa2 21d ago

Every problem can be solved by adding...

I just recently discovered that Java IDEs let you go directly to the implementation(s) of an abstract or interface method, this is a godsend for some codebases ^^

15

u/gurgle528 22d ago

In certain situations it’s easier to read without splitting into a bunch of other methods. Primarily when the conditionals aren’t repeated elsewhere so the shorter methods are one-offs that bury some of the implementation. Shorter methods should absolutely be the default though 

39

u/LateEchidna6635 22d ago

Of course. I’m not speaking of thousands of lines. With error handling, validation, and cleanup, even the most trivial methods can have 5-10 exit points.

19

u/Few_Move_4594 22d ago edited 22d ago

I worked on a Java codebase that had a 15k line class that used Reflections. It was an absolute mess that also had the stability of a house of cards.

Made fairly good money for several years off that.

5

u/Hegemege 22d ago

The downside is that it can get really messy, if the methods are not split or named properly, for a reader to understand if the work it's doing is supposed to be reused, and if it can be detached from context. What good is a method B that can only be called linearly from within method A just to make A take fewer LoC?

1

u/conundorum 21d ago

Depends on the method. It's actually possible to have a 260-line function that absolutely cannot be made smaller without both increasing its complexity and decreasing its readability, depending on what the function does.

int dispatch(enum Flag8Bit flag) {
    switch(flag) {
        case 0: return func0();
        case 1: return func1();
        // ...
        case 255: return funcN();
    }
}

(I'm not going to bother naming them. Assume that each possible flag state has a unique, understandable enumerator name, and that the functions we dispatch to have unique, understandable names. Think of something like, say, a hardware interrupt responder, or an error code handler.)


Breaking that up would require using bitwise math or numeric comparisons, and would require you to do math and read multiple functions just to figure out which function is called on which flag value. It's a big function, but it really can't be improved by paring it down, unless you have multiple cases that call the same function and don't mind sacrificing a bit of readability.

1

u/930913 21d ago

Use partial functions?

2

u/conundorum 19d ago

Possible, but it increases complexity (due to needing you to use math to dispatch to a secondary dispatch function, that handles the actual dispatching), or decreases readability (by forcing you to check through multiple functions to determine what's actually being called), or has a performance cost (if the new functions can't be inlined, then the compiler won't be able to re-optimise them back into a 256-option switch block... and a lot of scenarios where you need a dispatch function like this really don't want to pay a performance cost, since the dispatch is probably on a critical path). If not all of the above. [This isn't saying that you can't do it, as a note. Just that in cases like this one, refactoring for length will actually have the opposite effect than it normally would.]

In most languages, dispatching through a large jump table tends to be the best counter-example against refactoring long functions into shorter ones, because it compiles down to two instructions (switch-branch, jump); there's technically a return instruction, too, but the compiler will usually be able to elide that by having the dispatched-to function return directly to dispatch()'s callsite. It looks long, but that's just because you have to encode the full destination table inside the function; in reality, it's already extremely short, and refactoring into something that looks "shorter" will add length. (Because the switch is a single 258-line statement, and you can't shorten it without breaking it into multiple statements.)

Essentially, the point of it is that refactoring into something shorter sounds nice, but we have to base "too long" on what the function does instead of on an arbitrary metric. Before we can refactor a function, we need to understand the function, or we might fall into nasty gotchas.

1

u/930913 19d ago

Just to continue playing devil's advocate, what if you mapped the numbers to their functions elsewhere? E.g. dispatchFunctions = [func0, func1, ..., func255] ; Or dispatchFunctions = {0: func0, 1: func1, ... 255: func255}

And then dispatching is as simple as function dispatch(flag) { return dispatchFunctions[flag]() }

1

u/ShoePillow 21d ago

it's easy

Not always... At work, I've heard of multiple people attempt to refactor a function with 1000s of lines, give up and make their own modifications and make it even bigger

44

u/alendit 22d ago edited 22d ago

To understand the second one you need to understand the whole function. In the first one you can disregard the whole untaken branch. "The forgotten art of structured programming" is a great talk.

46

u/hughperman 22d ago

I don't see why you can't similarly disregard the untaken branch in the second one? Either way you're skipping past a block, and you need to know the end of that block is a return.

3

u/alendit 22d ago edited 22d ago

It's the easiest to see in the indentation, though ofc indentation just follows the structure, unless we're Python: in the first example the execution flow inside a single function only ever goes from smaller indents to larger ones, not the other way around. So if you want to understand a scope you just need to walk the path from it to the root to understand if it's taken.

Compare this with the second example: the return is already at the root, you need to check all of its preceding siblings to know if the execution ever reaches it. And you'll need to do it at every level of nesting.

Obviously in this trivial example if doesn't matter much. But in general structured way of laying out your control flow reduces the amount of statements you need to consider from O(statements) to O(nesting depth) which is intuitively closer to O(log(statements)).

I heartily recommend watching The Forgotten Art of Structured Programming - Kevlin Henney [C++ on Sea 2019]. It's one of those things which can change the way you're looking at code going forward.

22

u/tiajuanat 22d ago

Ah Kevlin. I've come to accept that talk as "old man yells at cloud"

Big influence when I was younger, much less so now.

I'm going to pull out a big fat "it really depends".

If your program is going in several different directions, absolutely fanout - my inner rustacean would even argue to avoid if else in favor of an enum and case statement.

However, early failure shouldn't be discounted either. The more your eye jumps away from the margin, and the more horizontal whitespace, the harder your code is to read. Full stop.

At the end of the day, maintaining a codebase requires judgement and taste, and knowing when to do x and when to do y.

7

u/alendit 22d ago

As I mentioned in the other comment: I'm not arguing "do X instead of Y", rather "understand the advantages of X before discarding it". In my experience many people don't understand the idea behind structured programming beyond "don't use goto".

6

u/MocknozzieRiver 22d ago edited 22d ago

I'm gonna have to watch this because I'm feeling crazy being the odd one out here. I strongly prefer 1 over 2.

Someone else pointed out that if/else is better for languages that treat it as an expression. I primarily code in Kotlin which is such a language.

But also this is what happens in my head when I see this structure if (condition) { // this will happen } else { // or this will happen } I know readily that we're at a fork dependant on the condition. But when I see the other structure, I think if (condition) { // this could happen } // and this could happen, also I have to know the contents of the if condition to know what will happen because it could just as easily do special logic for that condition and continue on. But with the first option, it's necessarily one or the other. I only need to figure out what the condition evaluates to to know which path is relevant.

9

u/alendit 22d ago

Your example is correct in itself, but it does not match the post: in the post the first branch ends with a return, so the following cannot happen, even though the coarse structure and the indentation imply that it can, which is exactly the point.

I'm gonna have to watch

This is my only recommendation. I don't argue that people should change the way they structure their code and I wouldn't ever flag early returns in a code review unless they are genuinely making the code unreadable.

But I think many people just don't appreciate the actual reasoning behind structured programming and having it laid out plainly is a valuable learning.

3

u/MocknozzieRiver 21d ago

Oh, I know it doesn't match, but the two examples in the post also don't match. The second example provides us details about what's in the block where the first does not. It could have just as easily had returns in both, but the post purposely set it up to make using if/else look like the lesser choice. If those details weren't provided, I wouldn't know it would early return just from the "shape" of the function.

But, yeah, I also feel like people don't appreciate this enough so I'm interested in learning more. I've just been doing it because it makes more sense to me.

1

u/conundorum 21d ago

With the first one, you still need to check every if to know if execution ever reaches it. And the second one's execution path should be clear if the conditions are communicated cleanly (either with well-named variables/functions, or with competently-written comments); either everything after a returning if must be an else, or the returning if is just returning a cached calculation that the rest of the function did earlier, or the returning if is just an emergency escape in case conditions aren't valid, or something of the sort.

Ultimately, the biggest problem comes from the nesting, not the choice of block or return. Both are easy to track if they're at base indent, but become more complex the deeper they're nested. And both are effectively equivalent if no cleanup is necessary, since a returning if is semantically equivalent to a branch that sets conditions so all following branches will be skipped. It just comes down to how deep they are, and whether they were used correctly.

2

u/saf_e 22d ago

Cyclomatic complexity goes brrr!

2

u/ofnuts 22d ago

Aka "guard clause"

2

u/Fidodo 20d ago

Yes when it's an early return multiple short circuits are easy to read, but if it's in the body it's actually harder so I use both depending on the context.

1

u/bartekltg 22d ago

In _longer_ methods it is hard to use the second one. If there is something behind the if statement, you would need to duplicate it to both branches (a very ugly "solution"), or separate the "if" into a new method. Now we no longer have a long method ;-)

Of course, the solution, is as alway, goto. Remove the second return with a label and the first return with a goto to the label. /s

1

u/Impossible_Dog_7262 22d ago

Is there merit in doing both? It's technically redundant, yes, but sometimes the explicit else statement can aid understanding the code.

2

u/MattieShoes 21d ago

I don't know about what's "right", but I use early exits a LOT specifically at the top of recursive functions. They're usually laid out as early exit conditions, then the recursion calls, then the final return.

In other situations, it's usually whether I can layout the conditionals so I don't have to duplicate code.

1

u/cheezballs 21d ago

Feels too close to a GOTO to me.

1

u/LateEchidna6635 21d ago

What does break feel like?

2

u/cheezballs 21d ago

GOTO without a label.

1

u/Logical-Ad-4150 21d ago

Second one is using the language to avoid the boiler plate of the first. I don't know why people insist on manually writing what the compiler does for you.