Introduction
Right now, you can make a function foo
take a function that optionally throws, and only throw if the given function also throws, like this:
func foo(_ body: () throws -> ()) rethrows {}
However, you can not make a function take an object with a method that optionally throws and only throw if it throws:
// Not currently supported
func foo<T: OptionallyThrowingProtocol>(_ body: T) throws follows T {}
This pitch would allow you to do that
Motivation
Things like directories and files seem like perfect classes to conform to Sequence
(a directory yields directory entries, a file is a UInt8
sequence) but if you actually try to write the conformance, you run into an issue: iterating over a directory or reading from a file can throw, and an IteratorProtocol
's next method cannot.
Currently, the only solutions are:
- Make your class do something other than throwing on errors
- Ending iteration early hides errors (not good)
-
fatalError
is also a bad idea
- Change
Sequence
to require its iterators to throw- Makes everyone
try
when iterating sequences, even if they're just iterating aRange
- Makes everyone
- Make a second version of Sequence that allows its iterators to throw
- Lose out on all the stuff Sequence magically gets you
This would also be useful if you wanted to make a struct that represents a number inputted by a user, whose operators throw instead of fatalError
-ing on overflow (but still conforms to Numeric).
In addition, while I think the need for this on throws
isn't quite as high, two other function annotations (async
and pure
) that have been proposed recently would benefit even more from this being an option (especially pure
)
Proposed Solution
Note: There are a lot of places where I'm not sure what the right balance in number of options is, so I'll present one option and add possible alternatives below. In addition the syntax is just there to demonstrate things you should be able to do, so if you can think of a better one please mention it.
All examples are shown with throws
but should be extendable to async
in the future. pure
would have to work slightly differently since pure
works the opposite of throws
or async
, a function throws
if anything it relies on throws
but a function is pure
if all of the things it relies on are pure
. impure
would match how throws
and async
work but that would be horribly source breaking.
Allow protocols to declare themselves as optionally throwing
A protocol should be able to declare itself as containing optionally throwing methods.
protocol Sequence throws {}
(Maybe optionally throws
would make it more obvious as to what this means?)
Allow protocols to define methods that throw optionally
An optionally-throwing protocol should be able to define optionally-throwing methods
protocol Sequence throws {
// makeIterator can throw if the `Sequence` conformance is throwing
func makeIterator() throws follows Self -> Iterator
// underestimatedCount can never throw
var underestimatedCount: Int { get }
// throwyMethod always throws
func throwyMethod() throws
}
Other options:
- All methods on an optionally-throwing protocol can optionally throw (no annotation required)
- There is no such thing as an always throws method, optionally throwing protocols can only have optionally throwing methods
Allow structs/classes to conform to the protocol
A struct should be able to express that it conforms to the optionally-throwing protocol either in a throwing or non-throwing way
// Conforms to Sequence and OtherProtocol with methods that don't throw
struct Array: Sequence, OtherProtocol {}
// Conforms to Sequence with methods that throw
// and OtherProtocol with methods that don't throw
struct File: Sequence throws, OtherProtocol {}
// Requires conformers to conform to Sequence, which they can
// choose to conform to throwingly or not
protocol MyProtocol: Sequence throws {}
// Requires conformers to conform to Sequence,
// only allows that conformance to be throwing if MyProtocol2 conformance
// is throwing
protocol MyProtocol2 throws: Sequence throws follows Self {}
For the two protocol examples, we may want to only allow the second variant (and make it just written protocol MyProtocol2 throws: Sequence throws {}
)
Express a type or function whose throws
is conditional on another
A type or function should be able to express that whether or not it is the throwing variant depends on some set of other types
// randomFunction takes a sequence and throws if
// that sequence is a throwing variant
func randomFunction<S: Sequence throws>(_ seq: S) throws follows S {}
// Usage
randomFunction([2, 3])
try randomFunction(File(path: "test.txt"))
// The use of `Sequence throws` is to keep source compatibility
// for existing `Sequence` functions that don't expect their sequence
// methods to throw. I don't especially like it, so if anyone else can
// think of a better solution that would be great
protocol Sequence throws {
// Iterator can (optionally) be the throwing variant if Self is the throwing variant
// For `pure`, this would be non-optional
associatedtype Iterator throws follows Self
}
extension Sequence {
// Throws if `Self` is a throwing variant
func randomMethod() throws follows Self {}
// Throws if `Self.Iterator` is a throwing variant
func otherRandomMethod() throws follows Iterator {}
// Throws if `Self` is a throwing variant or if `Other`
// is a throwing variant (for `pure` this would be "and")
func combine<Other: Sequence throws>(with other: Other)
throws follows Self, Other -> [Element] where Element == Other.element {}
}
// Zip2Sequence conforms to Sequence throwingly if either of its two base sequence throws
// but conforms non-throwingly otherwise (for `pure` this would be "both of")
struct Zip2Sequence<A: Sequence throws, B: Sequence throws>: Sequence throws follows A, B {
struct Iterator: IteratorProtocol throws follows A.Iterator, B.Iterator {
func next() throws follows A.Iterator, B.Iterator -> Element? {}
}
}
// Not sure if this is something we want to implement,
// but a way to do this could also be nice:
protocol MyProtocol: A throws, B throws {
// throws if the conformance to A throws, but doesn't care if the conformance to B throws
func a() throws follows Self.A {}
// I realize that you can't normally reference required conformances with `Self.A`,
// maybe `Self as A` would be better?
}
I think(?) this covers all the ways you could use a conditionally throwing protocol
One possible extension would be to change the syntax of rethrows
to be included within whatever syntax ends up being used for this.
Also, I realize that the above syntax gets confusing when you have multiple of these (especially the multiple protocol situation, at least throws
and async
are keywords):
struct Zip2Sequence<A: Sequence throws, B: Sequence throws>
: Sequence throws follows A, B async follows A, B, OtherProtocol throws follows A, B async follows A, B
One solution would be to require parentheses when combining multiple:
struct Zip2Sequence<A: Sequence throws, B: Sequence throws>
: Sequence (throws follows A, B) (async follows A, B), OtherProtocol (throws follows A, B) (async follows A, B)
Another would be to use a different character to combine multiple, for example &
struct Zip2Sequence<A: Sequence throws, B: Sequence throws>
: Sequence throws follows A & B async follows A & B, OtherProtocol throws follows A & B async follows A & B
Another would be to use a different syntax altogether (please suggest one)
Usage
Using types that conformed to protocols non-throwingly would be the same as it is now:
let a = [1, 2, 3, 4, 5]
for x in a {}
Using optionally throwing methods on types that conformed to the protocol would require try
as expected. A Sequence
conformance would require the addition of a throwing for loop:
let dir = Directory(path: "/")
for try file in dir {}
Same goes for async
let website = RemoteFile(url: "https://swift.org")
for try await byte in website {}
A File
could have a single
write<S: Sequence throws async>(contentsOf seq: S) throws async follows S where S.Element == UInt8
method that could be used to do any of these things:
let file = File(path: "/tmp/test.txt")
try file.write(contentsOf: [1, 2, 3, 4])
try file.write(contentsOf: File(path: "/tmp/test2.txt"))
try await file.write(contentsOf: RemoteFile(url: "https://swift.org"))
Effect on ABI Stability
While adding the ability to have throwing protocols wouldn't affect ABI stability, changing stdlib protocols to be optionally throwing almost definitely would
One possible way to get around this would be to have an annotation that caused the compiler to also generate a non-throwing version of the protocol runtime information along with dummy functions that forwarded to the optionally throwing variants.
Another way would be to create new protocols for the throwing versions and move functions over to them, though I feel like this would be kind of awkward to use.
Future direction
As mentioned above, as things like async
and pure
come into the language I would love to see this allowed for them too (being able to zip
two async iterators without having to make a separate async version of zip
, for example, is something that would be very nice to be able to do)
One other consideration is whether or not we'd want to allow something like this with mutating
. Outside of replacing the need for MutableCollection
, I can't think of any uses for this but maybe someone else has run into issues where they wished a protocol optionally allowed methods to be mutating.