[Amendment] SE-0296: Allow overloads that differ only in async

As I noted earlier, it's possible that there are source-compatibility issues here. It's already possible to have throwing and non-throwing variants of a function co-exist in an overload set, if they come from disjoint modules. Applying the async overloading to throws will change the meaning of some code. Here's a very contrived case:

// module A
public func f(_: Int) throws -> Int { ... }

// module B
public func f(_: Double) -> Int { ... }

// module C
import A
import B

func g() {
  try! f(0) // currently calls A.f, will switch to calling B.f because we're in a non-`throws` context
}

I don't know how much it will matter in practice. Maybe we can get away with just making the change, or maybe we need to do it with a language version bump.

Regardless, I think applying this overloading rule to throws is a reasonable thing to consider, but I think it needs to be a separate proposal, because (1) throws has source-compatibility constraints that async need not worry about, and (2) async already has the "prefer to match on async-ness when type checking" overloading rule (that was part of SE-0296) that throws lacks.

Doug

6 Likes

Makes sense, you're right that there are considerations to figure out.

I still feel that taking this proposal without taking throws would introduce complexity in the language and it would be cleaner to take both at the same time. I see the concern above as rationale to figure out the model here, not to defer consistency.

While I agree that overloading on what is returned is semi-dubious, overloading on what is thrown seems to naturally fall out of desugaring throws to Result:

func foo() throws ErrorA -> Int { 0 }
func foo() throws ErrorB -> Int { 0 }
func foo() -> Result<Int, ErrorA> { .success(0) }
func foo() -> Result<Int, ErrorB> { .success(0) }

I think the rules for effects and return types should be the same. This also is another reason for async overloading to be allowed if we think of async desugaring to Task.Handle/Future (or whatever it's called right now).

func foo() async -> Int { 0 }
func foo() async -> String { "a" }
func foo() -> Task.Handle<Int> { Task { 0 } }
func foo() -> Task.Handle<String> { Task { "a" } }

While I agree on not allowing typed throw type changes in a world with typed throws, isn’t a function that doesn’t throw the same thing as throws Never, so allowing the basic case of signatures that differ in throws only is already technically doing that?

4 Likes

I would call these two things functionally similar rather than technically the same.

2 Likes

Correct. Unlike other languages, swift's effects (throws and async) are NOT sugar over a more verbose model. They are similar and interoperate, but are intentionally different.

Apologies for introducing that confusion. What I was “remembering” was not how it actually is but one possible direction for the design discussed in the Typed Throws pitch.

Hello Swift community,

The Core Team has discussed this amendment as has decided to accept. The announcement is here.

Doug

4 Likes