[swift-evolution] Marking sort and sorted with rethrows
Brent Royal-Gordon
brent at architechies.com
Thu Jun 9 01:25:01 CDT 2016
> I'm not sure that these ideas are consistent with the Swift
> error-handling philosophy, which IIUC is very consciously designed *not*
> to support things like file- and database-backed Collections. My
> understanding is that if you have something like that, you're not
> supposed to throw errors on failure, but instead find some alternative
> means of error handling. These cases seem very much in the same
> ballpark.
I'm not talking about the Collection itself being backed by a file, but rather the instances inside it being backed by a file. I suppose you're trying to acknowledge that distinction when you say these cases are "in the same ballpark", but I don't think that's really a sustainable objection. There *are* sensible alternatives, like the early-terminating block pattern, to throwing sequences and perhaps even throwing collections:
try database.withQuery(sql) { rows in
for row in rows {
// If fetching a row fails, this loop terminates early and `withQuery` throws
…
}
}
But throwing operations on stuff-inside-a-collection? That's just part of the language, and making errors during sorting extraordinarily difficult to handle won't stop the errors from being possible.
* * *
I actually think there *is* a sensible way to define the behavior of a throwing `sort(_:)`: you treat the comparison as though it returned `false` and continue sorting. If there's any sort of stability to the errors being thrown by `isOrderedBefore`, this will end up behaving like a sort with unordered elements, much like an array of floating-point types with some NaNs in it. You not be able to rethrow all of the errors encountered during the sort—you'd have to pick the first or last—but a representative error would probably cover the use cases here.
You could implement this with today's Swift in terms of our existing `sort(_:)`:
mutating func sort(isOrderedBefore: (Element, Element) throws -> Bool) throws {
var lastError: Error?
sort {
do {
return try isOrderedBefore($0, $1)
}
catch {
lastError = error
return false
}
}
if lastError = lastError { throw lastError }
}
I don't think you could currently do one version with `rethrows`, because any way you do this, you'll need to save an error in a variable and `throw` it later, which I don't believe `rethrows` will permit. However, I saw a suggestion in the bottom type thread that, given both a `throws` keyword that can take an error type and a bottom type to represent the error type of a non-throwing function, `rethrows` could essentially be syntactic sugar for a function with a generic error type. This would give us additional flexibility in cases like this where `rethrows` doesn't quite do what we need.
With the necessary features in place, we could implement a `rethrows`-ish `sort(_:)` like this:
// This declaration has rethrows-like behavior: if `isOrderedBefore`'s error type is `Never`, then
// `sort(_:)`'s error type is also `Never`, and there's no reason to require a `try` on the call to this method.
mutating func sort<Error: ErrorProtocol>(isOrderedBefore: (Element, Element) throws Error -> Bool) throws Error {
var lastError: Error?
func isOrderedBeforeWithUnorderedErrors(a: Element, b: Element) -> Bool {
do {
return try isOrderedBefore(a, b)
}
catch {
lastError = error
return false
}
}
// Do the actual sorting here.
if lastError = lastError { throw lastError }
}
With a fair bit of work, it might even be possible to allow it to throw *all* of the errors it encountered:
// This is structured slightly strangely to ensure that, if Error is Never, then MultipleErrors<Never>
// is an empty enum (since all cases require a Never associated value), and thus (hopefully!)
// is itself a synonym for Never.
enum MultipleErrors<Error: ErrorProtocol>: ErrorProtocol {
case one (Error)
indirect case many (Error, ErrorList<Error>)
func appending(_ newError: Error) -> MultipleErrors<Error> {
switch self {
case let .one(oldError):
return .many(oldError, .one(newError))
case let .many(firstError, restErrors):
return .many(firstError, restErrors.appending(newError))
}
}
}
extension<Error: ErrorProtocol> Optional where Wrapped == MultipleErrors<Error> {
func appending(_ newError: Error) -> MultipleErrors<Error>? {
switch self {
case .none:
return .some(.one(newError))
case .some(let errors):
return .some(errors.appending(newError))
}
}
}
mutating func sort<Error: ErrorProtocol>(isOrderedBefore: (Element, Element) throws Error -> Bool) throws MultipleErrors<Error> {
var errors: MultipleErrors<Error>?
func isOrderedBeforeWithUnorderedErrors(a: Element, b: Element) -> Bool {
do {
return try isOrderedBefore(a, b)
}
catch {
errors = errors.appending(error)
return false
}
}
// Do the actual sorting here.
if errors = errors { throw errors }
}
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list