I don't mind early returns, but I'm infamously anti "break" and "continue" statements (and all my colleagues to date have disagreed with me and have found my stance hilarious and/or infuriating). My rationale is that they turn a loop into a mutant abomination that's a loop and conditionals all mushed up together. I also can't shake this gut feeling that they're crude and antiquated control flow mechanisms, only one step away from the fiery inferno of "goto". So instead of this:
for thing in things:
if thing.is_yellow():
continue
if thing.is_rainbow():
break
thing.thingify()
I'll do:
things_iter = iter(things)
thing = next(things_iter, None)
is_rainbow_thing_found = (
thing.is_rainbow()
if thing is not None
else False
)
while (
thing is not None
and not is_rainbow_thing_found
):
if not thing.is_yellow():
thing.thingify()
thing = next(things_iter, None)
is_rainbow_thing_found = (
thing.is_rainbow()
if thing is not None
else False
)
I almost never say this: you are wrong. Code needs to be maintained. Everyone can understand the first example. Everyone has to slowdown and reason about your second example. It is objectively worse. And your argument is flawed: break and continue are not the goto-like behavior that goto is criticized for as they are limited to the scope of that block of code.
The problem traditionally cited with goto is it can go anywhere (even into the body of another function where expectations on which variables are set may not hold).
If you don't like break and continue, how do you justify exceptions? The handling code for those may be in a different file or nowhere. Much closer to goto.
As others have said here, if you want to do it nicely and avoid break / continue, then do it in a proper FP way with chained limit / filter calls, rather than doing it per my example.
I accept your criticism, that my example is much harder to understand, and I admit that it's probably not the way to go in terms of maintainability. However, I'd still argue that my example is closer to an FP approach (even though it still uses a procedural loop), and that it communicates the logic in the same way that an FP approach does. In my example, it's clear that we're filtering out yellow (whereas with "continue", the logic is written in an inverted way); and it's clear just from reading the loop's header that we stop looping when rainbow is found (whereas with "break", logic to stop looping is being added at any arbitrary spot inside the loop's body).
Re: exceptions. I know that many more people object to them, than to break / continue (and whole languages make a point of not using them, most notably Golang). The pros and cons of exceptions is a whole topic in itself that I'd rather not digress into right here. Anyway, personally I'm ok with them. Firstly, they're for errors, and I feel that having weird control flow is sort-of justified for errors, whereas it's not justified for expected behaviour. And secondly, they're at least consistent in that they can occur anywhere, whereas break / continue can only occur inside loops (in Python, at least - in other C-family languages, "break" has different behaviour in different contexts, which is IMHO even worse).
Surely you are joking? Your version is four times longer and repeats a large section of itself.
The way to avoid control flow here is to simply find the rainbow first, take the sequence up until then, filter by yellow, and thingify the remaining things.
rainbow_idx = next(i for i, t in enumerate(things) if t.is_rbow())
yellows = (t for t in things[:rainbow_idx] if t.is_ylw())
for t in yellows:
t.thngry()
Excuse formatting, cellphone.
Not sure this is any better than the obvious solution either.
Yes, I agree, that's the way to do it. Although it isn't always so obvious how to break it up into limit / filter steps, my example was quite trivial. But there should always be a way somehow.
Considering break and continue to be too powerful as control flow statements is a somewhat common opinion, in my experience. (I still use them, but only sparingly, and where I am quite sure that they're not confusing.)
That said, your second code block is unreadable to me -- even knowing what it does, it's just confusing. No way that that's more readable than making use of break here. If you don't want so many conditionals in the loop body, you can factor the first check out:
no_yellow_things = (t for t in things if not t.is_yellow())
for t in no_yellow_things:
if t.is_rainbow():
break
t.thingify()
That's the nicest counter-example to my example, thanks for that! I wasn't familiar with take_while() (looks like that's Rust, and looks like Python has a similar itertools.takewhile() ), TIL a neat FP version of break. My example was quite trivial, it's not always so obvious how to break up a convoluted loop, but it should always be possible.
I’m with your colleagues I’m afraid. The intent of the all-mushed-up-together version is really clear. I had to spend a long time looking at the longer version to figure out what was going on. I know which I’d prefer to maintain if I had to.
I value being able to grasp what the code does at a glance while scrolling through. Your first snippet is entering my brain like as if without even reading at all, all at once and instantly.
Your second example absolutely does not and I'm sure you know why.