I'm toying around with noncopyable types, and one pattern I am repeating a bunch is having a noncopyable type which becomes invalid if a particular method that is invoked on it throws an error. One example is parsing a stream, where if the parse is successful the stream is simply mutated, but if a parsing error is encountered, the stream is effectively invalidated and shouldn't be used further.
I'm wondering if this is just a me thing, or if other folks have similar use cases (and maybe have found a good pattern for dealing with this). Also curious if something like a consuming throws makes sense for Swift.
Here is an example of some code where this would help (where self and decoder are ~Copyable)
In this example, if this function was consuming throws, we could avoid the do/catch and the self = decoder.finish() in the guard block as well as guarantee that the stream is not used after a failed decoding attempt.
Absent a language feature, you could approximate this behavior by having a consuming function which returns the value back on the success path. That way, if it throws, the value is destroyed, but if it succeeds, the value is returned back to the caller for further use.
A language feature would also be compelling in the opposite situation, where you want the value to be consumed on success but remain valid if the operation raises an error. The "consume and return the value back" approach is less appealing here, because you would have to throw the value back as part of the error value, but since Errors are (at least today) required to be Copyable, this would entail boxing the value and using dynamic consumption via Optional.take or similar APIs to transfer ownership back to the caller.
I think if such a feature was implemented, a natural spelling for it could be:
struct S: ~Copyable {
// current spelling: consumes self unconditionally
consuming func foo1() throws { /* ... */ }
// new spelling: consumes self only if the function returns normally
consuming(return) func foo2() throws { /* ... */ }
// new spelling: consumes self only if the function exits via a thrown error
consuming(throw) func foo3() throws { /* ... */ }
}
The same principle could be applied to consuming parameters:
struct S: ~Copyable { /* ... */ }
// current spelling: consumes s unconditionally
func foo1(_ s: consuming S) throws { /* ... */ }
// new spelling: consumes s only if the function returns normally
func foo2(_ s: consuming(return) S) throws { /* ... */ }
// new spelling: consumes s only if the function exits via a thrown error
func foo1(_ s: consuming(throw) S) throws { /* ... */ }