Is this a generics bug or feature?

I have asked a similar question before, but this is a much shorter and clearer version of it.


Carefully study this simple program:

(EDIT: On line 3, note that <T> and <T: Any> is the same thing, since "typealias Any = protocol <>" is a protocol composition of no protocols, which means it is constraining T with the constraint of no constraint.)

func foo<T:       IntegerType>(v: T) { print("  Called foo<T:       IntegerType>(\(v))") }
func foo<T: FloatingPointType>(v: T) { print("  Called foo<T: FloatingPointType>(\(v))") }
func foo<T:               Any>(v: T) { print("  Called foo<T:               Any>(\(v))") }

func bar<T, R>(v: T, fn: T -> R) -> R {
    return fn(v)
}

func baz<T>(v: T) -> Void {
    return foo(v)
}

print("foo-test:")
foo(111)
foo(1.1)

print("bar-test:")
bar(222, fn: foo)
bar(2.2, fn: foo)

print("baz-test:")
baz(333)
baz(3.3)


Here's the results (Xcode 7 beta 2):

foo-test:
  Called foo<T:       IntegerType>(111)
  Called foo<T: FloatingPointType>(1.1)
bar-test:
  Called foo<T:       IntegerType>(222)
  Called foo<T: FloatingPointType>(2.2)
baz-test:
  Called foo<T:               Any>(333)
  Called foo<T:               Any>(3.3)


To me, foo-test and bar-test behave as expected, but baz-test surprises me.


Is the output of baz-test different than the other two because of a compiler bug?

If no, please explain (in as much detail you can) why baz-test and bar-test produce different outputs.

Accepted Reply

Hi Jens,


The compiler is only looking at the function signature when doing static overload resolution. In your original example, in the case of

func bar<T, R>(v: T, fn: T -> R) -> R

called with

bar(222, fn: foo)

it chooses to resolve foo to

func foo<T:       IntegerType>(v: T)

because it's the best match for the 'fn' parameter given that the first argument to bar() is an integer and both 'v' and the parameter to 'fn' have to have the same type.


In the case of baz<T>(v: T), when compiling the body of the function it determines the foo() to call based on what's known about T. In this case it's unconstrained, and the only foo that works here is foo<T:Any>(). The function is compiled independently of the context of any call, unlike a language like C++, where function templates are instantiated at each call site based on the types used in the call.


Hope this helps.


[edited to clarify why foo<T:IntegerType> is the best match]

Replies

Sorry about the delay....

So, maybe i cannot help you more than "Closure", but just to complement my answer:


Like said: When Swift will compile the code, will only decide with funcion will call, nothin more! Of course, Swift can do Generic specialization, and does, but only after decide with method should call and only for performance reasons.


Just think in this way:


func foo(a: Int) {...}
func foo(a: Double) {...}

foo(1) // Will call Int version.
foo(2.0) // Will call Double version.


Pretty obvious, right?


When compile the line "foo(1)" the compiler will decide with foo function will call, but not more! Just will pick the correct address of the correct function.

To be more clear, foo(a: Int) and foo(a: Double) are completely different functions!

When compile will look of some function foo with has a double argument will do in the same way it is looking a zzz function with some other argument.



func foo(a: Int) {...}
func foo(a: Double) {...}
func foo(a: T) {...}
foo(1) // Will call Int version.
foo(2.0) // Will call Double version.
foo(NSObject()) // Will call T version


Now compile will pick foo(a: T) for foo(NSObject()) call because is the best match.


And what about the code inside of the foo(a: T)? Doesn't matter! Because the swift compiler is now only looking for funcion signatures, not of generic specialization.


Because that:

func baz<T>(v: T) -> Void {
    return foo(v)
}


When Swift compiler try to find with function is the best match of foo(v) call, will only find the "Any" version! Is the only match in this context because T has no constraint to make the compiler decide something about the "v" variable.


And the similarity of "Any" and "T" with no constraints is because:

typealias Any = protocol<>


Always think in the call side when working with Swift generics.


But you probably already understand that!

Wallacy wrote:

And the similarity of "Any" and "T" with no constraints is because: typealias Any = protocol<>


Ah, I think there are some misunderstanding(s) here, I did already know that there is no difference at all between these two:

func foo<T: Any>(v: T) { ... }
func foo<T>(v: T) { ... }

(I added the Any protocol there in my original example code just for symmetry, clearity)

What I thought you meant was that these two would mean the same thing:

func foo<T>(v: T) { ... }
func foo(v: Any)  { ... }


Hence I wrote what I did above about that. Oh, well, never mind.

What I thought you meant was that these two would mean the same thing

func foo<T>(v: T) { ... }
func foo(v: Any)  { ... }


Yes, i mean this. Once v is a protocol with any implementation, T will be the same in this case because without constraint can be any protocol too...


But yes... never mind...

Thanks for the clear question and educational thread. It seems to me that the problem with what you're asking for is that it kills separate compilation of generics and makes it so you can change the behavior of a function (effectively passing arguments to it) in ways that are not reflected in its function signature. Your feature request made sense to me at first just looking at the code for `baz`, but now imagine `baz` is in another module, and you don't see its implementation code. So all you see is the signature, but as a caller of `baz` you could potentially change the way `baz` works by having overloads in scope. You'd want some way to annotate baz`s signature to say that... and that's exactly what protocols and constraints on T are doing.


Rob

Thanks, that's a good point. I guess the main reason why I'm having a hard time getting this straight in my head is due to unconsciously thinking in terms of C++ templates even though I know very very well that they are a completely different thing all together.