Manually select overloaded functions

I would like to pitch an idea of allowing the programmer to optionally inform the compiler of a function parameter's type at call site, instead of always having the compiler infer the type.

Motivation

Overloaded functions are common in Swift. All of them rely on the compiler to infer at call site. Some times, there is no ambiguity between overloaded functions, and the compiler can always reliably infer which function is the correct one. For example:

// function 1
func foo(bar: Int) {}
// function 2
func foo(bar: String) {}

foo(bar: 42)   // calls function 1
foo(bar: "42") // calls function 2

There is no ambiguity between function 1 and function 2, because an integer won't be mistaken as a string, neither the vice versa. foo(bar: 42) always calls function 1, and foo(bar: "42") always calls function 2. However, if there is a 3rd function that introduces ambiguity:

// function 3
func foo<T: StringProtocol>(bar: T) {}

foo(bar: "42") // still calls function 2

foo(bar: "42") still always calls function 2, never function 3. Although "42" conforms to StringProtocol, the compiler prefers concrete types, so it always chooses function 2 over function 3 when both are suitable candidates. There is no way of telling the compiler that it should choose function 3, but at least the preference is clear enough that the compiler can resolve the ambiguity, and the programmer can be certain that function 2 is called.

Now, consider the following 2 functions:

// function 4
func foo<T: Hashable>(bar: T) {}
// function 5
func foo<T: Comparable>(bar: T) {}

foo(bar: 3.14) // Error: Ambiguous use of 'foo(bar:)'

Both function 4 and function 5 are suitable candidates for foo(bar: 3.14), but unlike the ambiguity between function 2 and function 3, the compiler doesn't know which one to call. In addition, since both Hashable and Comparable can only be used as a generic constraint, you can't do something like this work around it:

foo(bar: 3.14 as Hashable)   // Error: Protocol 'Hashable' can only be
                             // used as a generic constraint because it
                             // has Self or associated type requirements
foo(bar: 3.14 as Comparable) // Error: Protocol 'Comparable' can only be
                             // used as a generic constraint because it
                             // has Self or associated type requirements

Proposed Solution:

I propose allowing the programmer to optionally inform the compiler which function to call. Syntactically, I have 3 potential designs in mind:

// Design 1: Overload the "as" keyword. 
// When "as SomeType" is used at the end of a parameter value in
// an overloaded function, the compiler doesn't cast the value to 
// SomeType. Instead, it looks among the overloads for a function 
// where the parameter is of SomeType. If such a function exists,
// it will be chosen by the compiler as the correct one to call,
// and the parameter value will be cast to SomeType if necessary.
foo(bar: 3.14 as Comparable) // calls function 5

// Design 2: Use the ":" notation.
// This design probably will create additional ambiguity for
// parameters without labels.
foo(bar: 3.14: Comparable)   // calls function 5

// Design 3: Use C-like typecasting notation.
foo(bar: (Comparable) 3.14)  // calls function 5

Additional Benefits

By allowing the programmer to choose which overloaded function to use, the compiler's preference for concrete types can be overridden. For example, a concrete function will be able to call its generic counterpart in its body. This can result in better code reuse in protocol-oriented programming.

2 Likes

Firstly, you can work around it with simple forwarding functions:

func foo<T: Hashable>(bar: T) { print("Hashable") }
func foo<T: Comparable>(bar: T) { print("Comparable") }

func foo1<T: Hashable>(bar: T) { foo(bar: bar) }
func foo2<T: Comparable>(bar: T) { foo(bar: bar) }

foo1(bar: 3.14) // "Hashable"
foo2(bar: 3.14) // "Comparable"

In the fullness of time, the way to write this will be:

foo(bar: 3.14 as some Hashable)
foo(bar: 3.14 as some Comparable)

In the current version of Swift, the compiler tells you that this is not yet implemented ("error: 'some' types are only implemented for the declared type of properties and the return type of functions"). But there is no new syntax required.

This reveals the second way in which you can select the overload in the current version of Swift:

func someHashable<T: Hashable>(_ x: T) -> some Hashable { x }
func someComparable<T: Comparable>(_ x: T) -> some Comparable { x }

foo(bar: someHashable(3.14))   // "Hashable"
foo(bar: someComparable(3.14)) // "Comparable"
7 Likes

I feel this is kind of like cheating. It also makes function overloading somewhat unnecessary. If the overloaded functions are forwarded like this, why not just not overload them in the first place?

This is just typecasting with extra steps, and using opaque types to work around generics seems like an overkill.

I apologize if I come across as criticizing these 2 approaches. It's not my intention. They are solid workarounds to the problem I posted. And I didn't know the 2nd way existed. Thank you for showing me it. However, they are really awkward, not swifty.

Why does 3.14 have to be cast as an opaque type? And why is a typecasting necessary? If the user is allowed to tell the compiler exactly which function to call, wouldn't it just eliminate the need for typecasting in this kind of situation, and be more efficient at both compile and run time?

1 Like

This reminds me of pattern matching function clauses in Elixir :)

The problem is, exactly as you say, that they were overloaded to begin with. By construction, your problem is that they should not have been made overloads, since you need to distinguish them. Therefore, distinguish them by making them not overloads.

Opaque types are conceptually just "reverse generics," so this kind of use is exactly what they are for. You can think of it as telling the compiler, "forget anything else you know about this type, you can only know that it conforms to some protocol."

What you should not do is try to box these values into an existential as you show (3.14 as Comparable). Even if we allowed protocols with associated types to be used as existential types, this would be precisely the wrong use for that feature. Runtime costs aside, it cannot work for the use you have here because the existential type Comparable cannot conform to the protocol Comparable even if Swift allowed that existential type to exist. * In fact, the major reason why such existential types are disallowed in Swift is exactly that users will attempt to do what you have shown and wonder why it does not work.

* This is simple to demonstrate. If A conforms to Comparable, it means that two values of type A (a1 and a2) can be compared to each other. If B conforms to Comparable, it means that two values of type B (b1 and b2) can be compared to each other. However, that does not mean that a1 as Comparable can be compared to b1 as Comparable; in other words, the existential type Comparable does not conform to the protocol Comparable merely because all its conforming types do.

In this scenario, as is literally "telling the compiler which function to call." This is a type coercion; no type casting is performed. This is a common misunderstanding about the as operator.


During the design process for opaque types, because of the symmetry between that feature and generics, the core team talked about how some notation can also be used as syntactic sugar for generics. ** Note that this is not yet approved, but it demonstrates the relationship between generics and opaque types:

func g<T: Comparable>(_: T)
// ...equivalent to:
func g(_: some Comparable)

Written out in this way, you can easily understand what is going on in your example and how disambiguation can be accomplished using some notation:

func foo(bar: some Hashable) { }
func foo(bar: some Comparable) { }

foo(bar: 3.14 as some Hashable)
foo(bar: 3.14 as some Comparable)

(As I mention in the previous post, one cannot yet write such code because some notation cannot be used currently except as the declared type of a property or the return type of a function. This was a pragmatic limitation for ease of implementation; in the meantime, you can obtain the same result by writing a function with opaque return type as shown previously.)

** And by the same token, we also discussed at that time how generic notation can be used to spell opaque types:

func f() -> some Comparable
// ...equivalent to:
func f() -> <T: Comparable> T
// ...equivalent to:
func f() -> <T> T where T: Comparable

// Potentially allowing more complex constraints like:
func f<T>(_: T) -> <U, V> (first: U, rest: V)
where T: Collection, T.Element == U, T.SubSequence == V
3 Likes

This seems like it doesn’t address the problem when you are not the writer of the API.

A good example of this is in UIKit. The UIKit methods for converting regions and points between views and coordinate spaces are overloaded. For example:

convert(_ point: CGPoint, to view: UIView?) -> CGPoint
convert(_ point: CGPoint, to coordinateSpace: UICoordinateSpace) -> CGPoint

When you access this with a non-optional UIView, the type checker prefers the UICoordinateSpace method rather than wrapping the view as an optional. The problem? If you know the view is in the same view hierarchy, the UIView method is 12 times faster than the UICoordinateSpace method as it can make simplifying assumptions that the coordinate space method cannot. There is actually a workaround for this, to wrap the view in a .some() to optionalize it, however is this always an option?

Ultimately a response that “this is a problem with the API” is correct, but doesn’t address the requirements of the user. These method should probably have never been overloaded due to the nature of the behaviour, and yet they are now baked in and extremely unlikely to change for platform reasons. This seems like the programmatic version of “it hurts when I do this” - “well then don’t do it”. We may require workarounds to perform these functions if we aren’t in control of the API.

1 Like

My query for the language peeps here... is casting a safe way to guarantee behaviour?

That is, if my type matches exactly the overload due to a cast, is it always guaranteed that this will match and fire the exact overload, rather than one that is say for a protocol that the type matches? For example, in the above example, the compiler prefers the UICoordinateSpace method as it matches that more closely than a method that requires “optionalization”. But if the type was exact, eg if the overload in UIKit had UIView rather than UIView?, would something cast as UIView always win over the UICoordinateSpace method? Also how does it work if it’s cast as a subclass of UIView? Does inheritance take priority over protocol conformance?

If we had a defined set of rules for which methods match first, casting may be considered the correct way to “manually select” the method. It would, however, be a little cumbersome to communicate.