Inconsistent function overload resolution between top level call vs call inside generic imp

Calls at top-level invoke the overloaded functions I would expect, but same calls from generic implementation fail to differentiate by type.

Using this code in a playground...

import Foundation

protocol P {}
extension String : P {}

func f(_ o: Any)		{ print("f(:Any) arg=\(type(of: o)):\(o)") }
func f<T:P>(_ o: T)		{ print("f<T:P>(:T) arg=\(type(of: o)):\(o)") }

struct Wrapper<T>{
	private let key: String
	private let defaultValue: T
	private var storedValue: T

	init(wrappedValue v: T, key k: String) { defaultValue = v ; storedValue = v ; key = k }

	var wrappedValue: T {
		get { print("get \(key) (\(String(reflecting: type(of: storedValue))))..."); let t = storedValue ; f(t) ; return t }
		set { print("set \(key) (\(String(reflecting: type(of: newValue))))..."); let t = newValue ; f(t) ; storedValue = t	}
	}
}

let i = 1, i2 = 2
let s = "str", s2 = "str2"

f(i)    // 1.
f(s)    // 2.

var wi = Wrapper(wrappedValue: i, key: "wi")
let i1 = wi.wrappedValue    // 3.
wi.wrappedValue = i2        // 4.

var ws = Wrapper(wrappedValue: s, key: "ws")
let s1 = ws.wrappedValue    // 5.
ws.wrappedValue = s2        // 6.

...I get...

f(:Any) arg=Int:1                 // [1]
f<T:P>(:T) arg=String:str         // [2] ok
get wi (Swift.Int)...
f(:Any) arg=Int:1                 // [3]
set wi (Swift.Int)...
f(:Any) arg=Int:2                 // [4]
get ws (Swift.String)...
f(:Any) arg=String:str            // [5] ๐Ÿค”
set ws (Swift.String)...
f(:Any) arg=String:str2           // [6] ๐Ÿค”

I expected f<T:P>(:T) to be invoked where argument is compliant with protocol P, and and f<T:Any>(:T) to be invoked for all other types of argument.

I made String adopt P. Call 2 invokes f<T:P>(:T) as expect, but the calls embedded in the Wrapper.wrappedValue setter and getter in 5 and 6 do not.

What am I not understanding, or is this a bug?
Swift 5.1 and 5.2

It is not a bug. The calls to f within wrappedValue are resolved statically at compile-time using the information known about T. There are no constraints on T, so the Any overload is used.

If you want dynamic dispatch at runtime, then constrain T to some protocol and call a requirement of that protocol.

The calls to f within wrappedValue are resolved statically at compile-time using the information known about T .

...which is what I want. Dynamic dispatch not needed.

There are no constraints on T , so the Any overload is used.

...which is surprising - given all the information known at compile time, why not make the same selection, as in the top level calls - I don't see the difference. I'd appreciate a pointer to where this behaviour is documented - the language guide and reference does not seem to reach the deeper nuances.

The background to this is trying out a UserDefault property wrapper where there isn't a single constrain on T that I can use: there are three trees of T - native plist types (string, number, date, data, dictionary, array) which I make conform to a PlistPersistable protocol which can yield and reanimate from a plist-native object, codable types that can be stored as data, and the rest, which can't be supported. I don't see how I could make a constraint on T that is effectively either PlistPersistable or Codable, and I don't know how to express that an extension to Codable could implement the requirements of PlistCompatible. (And two flavours of UserDefault is clunky.) Have you solved this kind of problem before, and if so, how?

IIRC, the compiler also has an option to use generic implementation of Wrapper<T>, as opposed to specialised implementation of Wrapper<String>. So the only consistent compile-time choice would be to not use the fact that T conforms to P even if the specialisation causes it to be so.

You could create a new protocol, and provide default implementation for Codable, as well as PlistPersistable. It may have some problem if a type conform to both, though.

Alternatively you could use as?:

if let value = value as? PlistPersistable {
  ...
} else if let value = value as? Codable {
  ...
}

You could create a new protocol, and provide default implementation for Codable , as well as PlistPersistable . It may have some problem if a type conform to both, though.

I originally wanted any Codable to be acceptable as-is as a T, but couldn't express this. I've tried a default implementation in extension PlistPersistable where Self : Codable as you suggested, and this now works, but at the price of requiring an explicit declaration that the Codable argument is also PlistPersistable. I had hoped that the compiler would automagically recognise that I've provided the glue to make any Codable acceptable as PlistPersistable without any extra declaration.

Finally, is there anywhere the behaviour at this level is documented?

Again, this decision is made at compile time. You must think like the compiler. Then, it becomes self-evident. The compiler must determine what f means (i.e., which function to call) in the context where it is written.

Where you write f(s) // 2., the compiler knows the static type of s to be String and knows the conformance of S to P; therefore, it can determine that you meant to call f<T: P>(_: T) at the time that f(s) is compiled.

Where you write let t = storedValue; f(t), the compiler knows the static type of t to be T, which has no constraints. Therefore, it can determine that you meant to call f(_: Any) at the time that f(t) is compiled. What else could it choose? It cannot choose the other overload, because T is not constrained to conform to P. It must make a choice, because it must compile the code, and Swift says that f(t) must be statically dispatched.

3 Likes

There are two possible designs, whether you want to accept or reject types that don't conform to either PlistPersistable or Codable.

If you want to accept the otherwise case (and maybe reject it at runtime), you can just use unconstraint T, then use the if-else block as shown above.

If you want to reject otherwise case, you'd need to define a commonality between Codable and PlistPersistable, and so you'd need a protocol. You could create two Wrappers, one for Codable, one for PlistPersistable, but that gets unyieldy very quickly.

Unfortunately, I don't know of any such thing. I remember what I wrote because this topic comes up relatively frequently.

The rule of thumb is to consider the conformance of T to be limited to what's being declared, and never think about the actual concrete type (because your code may not be specialized).

2 Likes

The thing to realize (which I remember took a while to get used to) is that Swift's generics is not like C++ templates. Think of the generic function as being compiled totally independent of any call site, not knowing anything more about a type parameter than the constraints you've put on it.

4 Likes

The thing to realize (which I remember took a while to get used to) is that Swift's generics is not like C++ templates. Think of the generic function as being compiled totally independent of any call site, not knowing anything more about a type parameter than the constraints you've put on it.

This does help (and I was into C++ templates 15 years ago). Yet it also seems slightly at odds with the compiler doing as much static optimisation as possible โ€” it seems like it misses a few tricks, but probably good reasons, etc.

I'm too stupid for that. :blush:

I repeat the request: where is the behaviour documented?

This is not about documentation as courtesy and kindness. Its about the vendor committing to make a contract for behaviour rather than avoiding the issue and being vague and ambivalent.

(I suppose if I really want to know the contract, I could digest and memorise everything here: swift/test at main ยท apple/swift ยท GitHub but :nauseated_face:)

The procedure at the moment is use up a lot of time without success, call for help, use up a lot of your time on questions that have been asked many times before.

(By the way, many thanks you for your help @Nevin, @Lantua, @xwu, @Jens :+1:)

So... contract?

The behavior is widely discussed; a simple Google search would give you many results. But if you want to read the most original statement, you can find it in the original design document for generics; the particular section on overloading dates back to at least 2012.

Missing the point. It's not the same thing as the vendors taking responsibility to make a clear statement, a clear commitment and a contract, and putting that in a place where it will definitely be found by those looking to understand and use Swift.

3 Likes