This is very like the example I concocted for the relevant educational note, which was recently discussed in another thread. The note needs to be updated for the implicitly opened existentials feature (or rather, a different version needs to be written for the new diagnostics), and that should be tracked by a GitHub issue—contributions welcome!
The difference between Adopt(anyDuck) and feed(anyDuck) is that Adopt<T>.init has type T -> Adopt<T>, while feed has type any T -> Void. If feed had type any T -> T, it would be similarly forbidden, because T can only be known at runtime, but once it appears in return position it becomes visible in the calling context’s static type environment.
feed(_:) doesn't know it is Donald, it just knows it is some Duck. it never needs to know if it is Donald because it can just use the type variable T to refer to it.
this is different from calling the initializer on (any Duck).Type, it escapes the the type variable Self to the caller's scope, and Self is not a valid type name there.
func feed<T: Duck>(_ v: T) {
print(T.self)
}
...
var anyDuck: any Duck = Donald()
feed(anyDuck)
I believe there's a separate compiled function for every type I'm calling it with, in this case "feed_Donald" (if it had a name), so the above print prints "Donald".
Are you talking about "walk", not "feed"? This compiles just fine:
func stuff<T: Duck>(_ v: T) -> T { v }
stuff(anyDuck) // ✅
1> let x: any BinaryInteger = 2
x: Int = 2
2> func f<T: BinaryInteger>(_ arg: T) -> T { return arg }
3> f(x)
error: repl.swift:3:1: error: inferred result type 'any BinaryInteger' requires explicit coercion due to loss of generic requirements
f(x)
^
as any BinaryInteger
yet:
3> protocol Duck { }
4> struct Donald: Duck { }
5> let anyDuck: any Duck = Donald()
anyDuck: Donald = {}
6> func stuff<T: Duck>(_ duck: T) -> T { return duck }
7> stuff(anyDuck)
$R0: Donald = {}
And for what it’s worth, the type(of: stuff(duck)) is any Duck, not Donald.
This is true of C++ templates and Rust generics, but not Swift. Swift passes the runtime representation of the type as a hidden parameter and has a fully polymorphic implementation ready to go. (Though it may get optimized to an implementation that just works for Donald.)
When you call a generic function, you provide a replacement type for the generic parameter 'T'. When you open an existential, the replacement type is a local type representing the payload of that exact existential value. This local type cannot "leak out" of the expression. If my function also returns 'T', then the actual return type of the call is this local type, but since that cannot be the type of an expression, we wrap it in an existential.
So there's a symmetry here: calling a function <T: P> (T) -> T with an any P value opens the existential, calls the function with the payload, and wraps the result inside of a new any P. (Statically, you've lost the fact that both any Ps now have the same concrete type inside)
The reason that this doesn't work with an initializer is that the type of the initializer is <T: P> (T) -> Foo<T>. The substituted return type is Foo<> applied to this local type, but there is no existential type to erase it to; Foo<any P> is not correct.
We could generalize existentials further, and allow you to write any <T: P> Foo<T> for an erased Foo<> with an unknown generic parameter. Then the return type of our hypothetical init applied to an existential value could be expressed. With this new spelling, the existing any P type is just a shorthand for any <Self: P> Self.
My recollection was that the leak was prevented by banning calls to generic functions where an existential must be implicitly opened to fill a type parameter that also appears in return position.
That seems to be happening for any BinaryInteger but not for any Duck for reasons I do not understand.
I misunderstood the part regarding BinaryInteger. That looks like a bug. In any case it should be safe to return T, or T?, or () -> T or any other type that contains T in covariant position, because type erasure allows us to plug the leak, so to speak.
Is there a special rule that allows an implicitly opened existential to substitute for T in T -> T? ? Because T? is shorthand for Optional<T>, which is not covariant in T.
After some distillery work, this is what makes BinaryInteger special:
protocol N {
associatedtype M: N
}
protocol UglyDuck : N where M : UglyDuck, M == M.M {}
let x: (any UglyDuck)? = nil
func stuff<T: UglyDuck>(_ duck: T) -> T { duck }
stuff(x!) // 🛑 Inferred result type 'any UglyDuck' requires explicit coercion due to loss of generic requirements
Also found another unrelated issue:
protocol Duck {}
var duckOpt: (any Duck)?
var duckIUO: (any Duck)!
func roast<T: Duck>(_ duck: T) -> T { duck }
roast(duckOpt!) // ✅
roast(duckIUO) // 🛑 Type 'any Duck' cannot conform to 'Duck'
roast(duckIUO!) // 🛑 Type 'any Duck' cannot conform to 'Duck'
That's a nice test case, thanks .It looks like the check that emits the "losing requirements" diagnostic digs through generic requirements and tries to figure something out. While it should be using generic signature operations instead of performing a syntactic analysis, the more fundamental issue is that the question it's trying to answer is probably unanswerable.
I think the written intent in the proposal is that the compiler must figure out, given a generic signature <T0, T1... where ...> and some type parameter Tn.A.B that's known to conform to a protocol P, whether the type parameter is "completely described" by its conformance to P, in the sense that anything we can derive about a member type of Tn.A.B or Tn.A.B itself, is a consequence of this conformance requirement only, and not any other requirement.
This is equivalent to asking if a certain function between two monoids is injective. This may or may not be decidable.
So maybe we can just remove this diagnostic, since it no longer seems to serve a clear purpose beyond introducing a theoretical puzzle.