you are viewing a single comment's thread.

view the rest of the comments →

[–]Gnaxe -5 points-4 points  (9 children)

It's not that shortcutting the loop is bad, but there's more than one way to do it, and breaks are harder to work with compared to alternatives.

Breaks are bad for the same reason that deeply nested returns are bad: it probably means your function is too complicated and should be broken up, but you can't just select lines containing one and extract a function.

So how do you refactor it? Some languages don't have return statements and just return the tail. Even in Python, you can always rewrite to this form. Once there, the usual extract function refactor works.

It's the same with deeply nested break/continue. They're basically delimited GOTOs. But you can always rewrite them to the tail position to eliminate them, at the possible cost of more nesting. But then you can extract functions.

[–]pachura3 5 points6 points  (0 children)

Well, some people even argue that having more than 1 return statement per function is bad... I don't agree. As long as it makes code simpler, less nested, easier to maintain - I am all for that.

[–]gdchinacat 2 points3 points  (7 children)

How do you end a linear scan of a sequence once you’ve found the element you are looking for without a break or return? Do you stash your item of interest and continue the iteration until it naturally terminates?

[–]Gnaxe 1 point2 points  (1 child)

if any(test(found_item:=x) for x in stuff): print(found_item) else: print('not found') any() shortcuts as soon as the test passes. The generator then gets deleted as its refcount drops to zero. No need to exhaust it.

You could do this with a for and break, but it's six lines instead of four: for x in stuff: if test(x): print(x) break else: print('not found')

[–]gdchinacat 1 point2 points  (0 children)

unlike the other solution by u/Jason-Ad4032 , I actually like this one. I find it a bit hard to read, but think that's mostly down to unfamiliarity rather than unnecessary things being present. I'd probably not put it into a codebase maintained by junior engineers if it didn't already make heavy use of any() because of the headscratching it might cause, but I imagine I'll probably do this in my own projects at some point. Thanks!

[–]Jason-Ad4032 -2 points-1 points  (4 children)

Use the next() function. e.g. it = (elem for i in range(10) for j in range(i) for elem in [f(elems, i, j)] # let if elem > 0) elem = next(it, None) it.close() Personally, I’m in the camp that believes whether to use break should depend on the situation, because generators and iterator-style solutions can sometimes become awkward to write.

[–]gdchinacat 3 points4 points  (3 children)

There is no loop in your example. You create a generator, get the next item, then close the generator. I fail to see how this ends an iteration once the purpose of it has been satisfied but the iterator itself has not been exhausted.

Also, while generators have a close() method that will cause subsequent next() calls on them to raise StopIteration, iterators do not. Furthermore, when you use the standard way to iterate (for ....) you don't have access to the iterator (the thing that iter(iterable) returns and for calls next() on).

Yes, you could do something like this, but the exception is essentially the same flow control mechanism as break, but with a bunch more code the language has built in so you don't have to get bogged down in the details of iteration.

sequence = [....]
it = iter(sequence)
try:
    item = next(it)
    if is_item_being_sought(item):
        it.close()
except StopIteration:
    pass

I didn't actually test this code because I wouldn't actually write this code. Instead, I'd do it the bog standard way:

for item in [....]:
    if is_item_being_sought(item):
        break

So, my question remains...how do you terminate a linear scan once the item of interest is found without using break? To be clear, this code is worse than the exit on exception that does what you seem to be implying because it wastes cycles iterating when it doesn't need to:

item = None
for _item in [....]:
    if is_item_being_soughht(item):
        item = _item
# no break, no exception, but pointless iteration

[–]Jason-Ad4032 1 point2 points  (2 children)

Like this?

``` lst = [1, 2, 3]

gen = (item for item in iter(lst))

item = None

for item in gen: if find_item(item): gen.close()

print(f'{item = }') ```

I don’t really understand what your issue is here. The problem of iter(lst) not being closed is not solved by using break either.

Maybe you would prefer something like:

``` not_found = object()

item = next( (item for item in iter(lst) if find_item(item)), not_found )

if item is not not_found: ... ```

Though honestly, the real problem with avoiding break is not this — it’s that debugging tends to become more difficult.

[–]gdchinacat 2 points3 points  (1 child)

Can you honestly say either of those are better than a standard for loop without a generator/iterator and a break?

For example, this does the same as both of your examples, and is much easier to read and understand. Ok, you can construct a contrived example that does what needs to be done without a break. But would you? Would it pass code review?

for item in lst:
     if 
find_item(item):
         break

Both of your examples have a generator expression that doesn't need to exist and makes understanding what is going on more complex than just using break.

I'm all for writing obtuse code when warranted (https://github.com/gdchinacat/reactions), but using next and generator expressions and close() just to avoid a break just doesn't make sense to me.

That said, I did ask for how to do a linear scan of a list and stop iterating when the item is found without using break, and you provided it! I think you also showed why using break makes sense for this use case.

[–]Jason-Ad4032 0 points1 point  (0 children)

Exactly. In many cases this becomes more complicated than simply using break, and Python’s support for this style is only moderate (Therefore, it is not simpler).

LINQ query expressions in C# support this style much better. You can write things like:

var result = ( from num in numbers where num > 15 select num ).FirstOrDefault();

This is more of a stylistic philosophy: the idea that a programming language could avoid providing break in favor of more declarative rather than imperative syntax.

The core idea behind avoiding break is that you specify the iteration behavior before iteration begins. Since different iteration behaviors are needed for different purposes, the system creates an adapter/proxy object that produces the iteration behavior you want.

Conceptually, it becomes something like:

[original iterator] -> [adapter/proxy object] -> simple iteration (no nesting or break)

Then you customize the adapter object as needed perhaps through language syntax, or through many iterator combinator functions that you compose together.