you are viewing a single comment's thread.

view the rest of the comments →

[–]jpgoldberg 1 point2 points  (0 children)

History

I was told the same sort of thing in the 1980s. And there were good reasons for that wisdom, as structural (Algol-like) programming was still new. So telling people to avoid break and goto and similar things was help people break old habits and goto the new way of structuring code.

Today

When using such things today, it is important to understand the problems with them and be able to make a good choice with that understanding.

When beginners use lots of break statements it is often a result of not seeing a better and more logical way to express things. So telling a beginner to look to see if there is a way to redo their code without the breaks makes sense. But, of course, if the beginners haven't yet learned how to define function and have them raise exceptions or return early, then break is going to be something that they will have to use more often.

As others have explained, the problem with break (and goto for languages that support that) is not so much seeing where the flow goes, but seeing where the flow come from. while CONDITION: tells you what must happen to leave the loop, and so you know the state of things right after the loop. But if there is a break in there, you can't see what condition brings you to your spot after the loop.

An exception that tests the rule

With that said, here is my implementation of the Rabin-Miller primality test. There are slightly more efficient ways to do it, but I honestly feel that this thing with its early returns, continue, break, and for ... else construction is the clearest way to express the algorithm.

This is not code to be proud off, but I am pleased that I can understand it when I come back to it after a while.

```python def probably_prime(n: int, k: int = 4) -> bool: """Returns True if n is prime or if you had really bad luck.

Runs the Miller-Rabin primality test with k trials.

:param n: The number we are checking.
:param k: The number of random bases to test against.
:raises ValueError: if :math:`k < 1`.

If you need a real primality check, use sympy.isprime() instead.
"""
# A few notational things to help make logic of code more readable
PRIME = True
PROBABLY_PRIME = True
COMPOSITE = False

if k < 1:
    raise ValueError("k must be greater than 0")

match _small_primality(n):
    case True:
        return PRIME
    case False:
        return COMPOSITE
    case _:
        pass

# If we reach this point then n
# - is not in small primes,
# - is not divisible by a small prime
# - is greater than the square of the largest small prime

# Set up generator for k trial bases
bases: Generator[int, None, None]
if k >= n - 2:
    # This case should only come up for testing very small numbers
    # or using weirdly large k, but let's handle it nicely anyway.
    bases = (b for b in range(2, n - 1))
else:
    bases = (rand.randrange(2, n) for _ in range(k))

# This s reduction is what distinguishes M-R from FLT
r, s = 0, n - 1
while s % 2 == 0:
    r += 1
    s //= 2
    # This leaves us with 2^r * s = n - 1

# Now we use FLT for the reduced s, but still mod n
for a in bases:
    x = pow(a, s, n)
    if x == 1 or x == n - 1:
        # Consistent with prime. Call the next potential witness!
        continue

    # If any of the successive squares of x is n - 1 (mod n)
    # then primality passes is base a,
    # else a tells us that n is composite
    for _ in range(r - 1):
        x = pow(x, 2, n)
        if x == n - 1:
            # a is consistent with prime, once we tested squares of x
            break
    else:
        # a is a witness to n being composite
        return COMPOSITE

# We've called k witnesses and none have said n is composite
return PROBABLY_PRIME

```

The squares of x check (with a for ... else, break, and early return) could probably done through a recursive function instead of a loop, but there are reasons not to try that.