I've been using the past few days to work on the code that inspired this thread, and more people are talking about exceptions. I have to stomp on these dreams now.
Exceptions are generally for things you can't handle. In other words: if you fail, you bail. They are not for general flow control.
I originally handled my prime-searching code with custom sequences. Then I moved to a special protocol with a fixed partner sequence. I switched to these custom operators in between. Here's my old Sieve of Eratosthenes:
public struct LimitedPrimalitySequence<Integer: FixedWidthInteger>: LazySequenceProtocol {
@inlinable public init() {}
public struct Iterator: IteratorProtocol {
@usableFromInline var upcoming: Integer?
@usableFromInline
var primeMultiples: PriorityQueue<(multiple: Integer, prime: Integer)>
@usableFromInline var pastSquareRoot = false
@inlinable init() {
upcoming = Integer(exactly: 2)
primeMultiples = PriorityQueue(priority: .min, by: <)
}
public mutating func next() -> (Integer, Bool)? {
guard let subject = upcoming else { return nil }
defer {
let (successor, failed) = subject.addingReportingOverflow(1)
upcoming = failed ? nil : successor
}
var gotPrime = true
while let (multiple, prime) = primeMultiples.first, multiple == subject {
// Indicate a composite was found.
gotPrime = false
// Move this prime factor to its next multiple.
let (nextMultiple, failed) = multiple.addingReportingOverflow(prime)
if failed {
// The counter will overflow before reaching the next
// multiple, so don't bother keeping it.
primeMultiples.removeFirst()
} else {
_ = primeMultiples.movedFirstAfterUpdating {
$0.multiple = nextMultiple
}
}
}
if gotPrime, !pastSquareRoot {
let (square, failed) = subject.multipliedReportingOverflow(by: subject)
if failed {
// Note that all new primes will have their first required
// multiple happen after the counter goes past Integer.max,
// so don't bother storing them.
pastSquareRoot = true
} else {
primeMultiples.push((multiple: square, prime: subject))
}
}
return (subject, gotPrime)
}
}
@inlinable public func makeIterator() -> Iterator { return Iterator() }
@inlinable
public var underestimatedCount: Int {
Integer.max < 2 ? 0 : Int(clamping: Integer.max - 1)
}
}
It uses the overflow dance. Here's the new version, as part of implementing a protocol.
public struct LimitedPriorityQueueEratosthenesSieve<Integer: FixedWidthInteger>: WheeledPrimeChecker {
typealias PrimeMultiple = (multiple: Integer, prime: Integer)
var primeMultiples = PriorityQueue<PrimeMultiple>(priority: .min, by: <)
var pastSquareRoot = false
@inlinable public init<S: Sequence>(basis: S) where S.Element == Integer {}
public mutating func isNextPrime(_ coprime: Integer) -> Bool {
var gotPrime = true
while let (multiple, prime) = primeMultiples.first, multiple <= coprime {
if multiple == coprime {
gotPrime = false
}
if let nextMultiple = multiple +? prime {
_ = primeMultiples.movedFirstAfterUpdating {
$0.multiple = nextMultiple
}
} else {
primeMultiples.removeFirst()
}
}
if gotPrime, !pastSquareRoot {
if let square = coprime *? coprime {
primeMultiples.push((multiple: square, prime: coprime))
} else {
pastSquareRoot = true
}
}
return gotPrime
}
}
In neither case do I bail on a wrap around; which means my needs here are not exceptional. On wrap-around, I take another branch instead to end some part of my iteration. And both branches are method-local. Since I don't kick the wrap-around to external code, a theoretical throwing operator would require a do/try/catch mechanism within the method. That exception dance would be even bigger than the overflow dance.
Plus, since IteratorProtocol.next can't throw, I would have to include an universal catch in the first version if I used throwing operators. Overall, Optional-handling is much better for me in this case.