[Pitch] Rethrowing protocol conformances

This is an interesting and cool extension, I like the general direction, but I agree with the mid-thread suggestion that using "rethrows" in the protocol decl was both too clever and unnecessary.

My understanding of this is that you want this to work with multiple protocol requirements, for example:

protocol P1 { 
  func f1() throws
  func f2() throws
}

and when implemented by:

struct S1 : P1 { func f1() {} func f2() {} }

and used by something like:

func genericThing<T : P1>(a : T) rethrows {
  try a.f1()
  try a.f2()
}

That a rethrowing function wouldn't be considered throwing.

A couple of questions about this:

  1. I assume this means that a type is considered "throwing" if any of the protocol requirements are fulfilled by a throwing function? I think this is true even if the genericThing doesn't actually use one of throwing requirements because we want the implementation of genericThing to be able to evolve over time.

  2. What happens with resilience when someone comes along and adds a new "f3" throwing requirement to P1 with a default implementation (that could throw)?

Does that break rethrow checking in genericThing? If so, how do we make it invalid to add new throwing requirements to protocols?

If this is an issue, then I think that "rethrow checking" is something that a protocol should opt into somehow, because you'd have to guarantee that you will never add a new throwing requirement in the future. This seems like an attribute that P1 would have to adopt or something.

-Chris

3 Likes

Yes, that's correct.

Yes, that's correct.

Yes, it could end up breaking rethrow checking. Good catch!

This is a great point. It is limiting how the protocol evolves: any newly-added throwing requirement must have a non-throwing default implementation.

Doug

1 Like

I just can’t shake the feeling that this is going to have so many unintended consequences. Can I declare a rethrows that only depends on some parameters? Is self special? Are protocol extensions different from concrete methods? How do I do wrapper structs, like @Lantua’s example? Is it all generic parameters or just the innermost ones?

It stinks because there are absolutely good use cases for this, most notably all the API on AsyncSequence. But it makes rethrows feel extremely complicated. When the source of rethrows-ness is “closure parameters that throw” it’s very obvious from looking at the function declaration which those are. Allowing that to include requirements of associated types of generic parameters (of enclosing contexts?) is just a little too much indirection for me.

I’d definitely feel better about it if throws was declared on each generic parameter that was a source of throwing requirements, including Self, as @Lantua played with. You’d have to allow a way to specify it other than on constraints for constraints that are already in scope (again, particularly relevant for Self). But having it be only some protocol constraints but not all is probably overkill.

So…how about parameterized rethrows? You can put the name of a generic parameter or a throwing closure parameter in the parentheses; the default (to not change behavior) is “all throwing closure parameters”. You could even put a specific requirement if you wanted.

extension FailableSequence {
  func first() rethrows(Self) -> Element { … }
  // or
  func first() rethrows(Iterator) -> Element { … }
  // or
  func first() rethrows(Iterator.next) -> Element { … }

  func map<Result>(_ transform: (Element) throws -> Result) rethrows(Self, transform) -> [Result] { … }
}

struct LazyMapSequence<Underlying: FailableSequence, Result> {
  func next() rethrows(Underlying) -> Result
}

This is a bit explicit, to be sure, but we can always come up with shorter forms later. I don’t think having the general form will turn out to be a problem as long as we don’t try to get too specific (for instance, depending on a particular requirement, which would then make adoption of new throwing requirements a source-breaking change).

Note that this still doesn’t allow perfect propagation of throws; in particular, there’s no good way to parameterize LazyMapSequence on the throwing-ness of the transform without further features. I do sometimes wish we had a more first-class effects system for things like this.

8 Likes

Self is not special; it is any parameter in the generic signature of the function that adopts a protocol that can be determined as a source of rethrowing. I have a branch with the functionality https://github.com/phausler/swift/tree/rethrow_protocol if you are interested in my current thoughts of how this can be implemented. Of course we are still working out the details.

Is there any movement on this?

I've been experimenting with the AsyncSequence code in the new concurrency framework and it seems all the functions written for AsyncSequence (eg reduce, min, max etc) have been written with the assumption that this pitch will be implemented.

Currently I have written my own reduce which throws instead of rethrows. Obviously I would prefer that the one implemented in Swift works and I could use that.

2 Likes

Just to note, this kind of granularity makes which parts of the protocol are used by any operation part of the operation's interface. It's very similar to noexcept in C++, which is in large part my fault and IIUC has been a failure because people never really know how much detail to expose. IMO it's actually better to start with a limited amount of detail (just as we did by only supporting rethrows with functions at first) and let the need for more become apparent.

Fair point. I think this is my primary concern:

Terms of Service

Privacy Policy

Cookie Policy