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

This sure seems broken to me but I seem to remember a discussion last year where the team said this behavior is by design because there are no constraints on T in baz. IMO static type information about the actual argument provided for the T parameter should flow through allowing this code to produce the results you expected.

I'm pretty sure this is the expected behavior. Swift resolves overloaded functions statically (just like all other popular OO languages). The function call on line 10 gets resolved to foo as defined on line 3. This is the only function that works for objects of all types T. The runtime type information isn't used for the resolution of overloaded functions.

This isn't about runtime type information. If Swift properly performed generic specialization it could make the correct call with static type information. This is essential to a sane generics implementation IMO. It should be guaranteed by the semantics of the language so we can rely on it.


I have to wonder if this code would behave differently when compiled under optimization if the optimizer chose to specialize the calls to baz on lines 22 and 23. If it does we have optimizer-dependent behavior which seems really bad. If it doesn't we have generic specialization that is really crippled.


While I am really excited about Swift, love to see Apple doing it, and know the team is working really hard to improve it rapidly, I have to say this kind of issue seems to pop up in many corners of the language and really drive me nuts!

Just to be clear, and as anandabits has already pointed out:


I certainly DO expect everything in there to be resolved by the compiler statically, so it's not at all about that.


It's just that I expect the compiler to do "better" than this (in the baz-test case), since it is possible for the compiler to deduce what different overloads of foo the different generic variants of baz should call. It depends on the type of baz's T (Int and Double in this case) and thus it could generate the specific generic versions of baz for each different T that the source code implies.


The way it's currently working (for the baz-test case), the baz function could in fact equally well have been written non-generically, like this:

func baz(v: Any) { foo(v) }


This will produce the same output as the original generic version, ie they both act as if they ignore the original statically knowable type information of the argument given to them and just calls the non-restricted foo. This behaviour is what I expect for the above non-generic ("dynamic") version of baz, but surely the original generic ("static") version of baz should be able to make some proper use of its T? That is, it should of course use its advantage of having that T in order to do "better" than the above non-generic version. What good is it having that T otherwise?


And again, if this is not a bug, but expected behaviour, how come bar-test and baz-test produce different results?

Yes, T with no constraint will behave exactly like "Any"!


Swift can do generic specialization (like shown in wwdc) but only for performance goal when optimize the code. But the final behavior is the same.


It's not like C++, which generates a specialized version of every single call.


Just to complement: The main problem is the "two levels" of generic abstraction.

When the compile the "baz" function, there's no constraint to make the compiler create a specialized version of "foo" function, so compile will decide call the "Any" version.

"bar" and "foo" don't have this problem.

Is the baz-test result different between debug build and fully optimized build?

Nope. Same result.

Wallacy wrote:

Yes, T with no constraint will behave exactly like "Any"!


Well, look at bar (not baz) in my original example, bar is working exactly as I expect, yet it too has a "T with no constraint". How do you explain that?


And how would you explain what happens below, when I replace the T in bar with "Any", just as I did with baz above (and which you say should make no difference):

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 bar<R>(v: Any, fn: Any -> R) -> R { return fn(v) }

func baz<T>(v: T) { 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)


Prints:

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


(Note what happened to the output of bar-test) This is exactly what I expect to happen, no surprise. Since bar lost its T, and thus the compiler can no longer statically infer the proper foo to call for the bar-test.


Now, I can change bar back to get it to behave in that "better" generic way again, but I can never get baz to behave in that "better" generic way ... How come?

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]

Thanks, that explanation (with that level of detail) was needed to let me understand the behavior of Swift's generics, and I guess a lot of people will be bitten by this. It's not enough to hear that it's not as C++, because it feels almost like a central part of what is "generics" is removed.


My gut reaction is to think: But why?! This is so crippling / severely limiting : O


But I'm sure there are advantages and good reasons behind it being designed this way, and now I'll just relearn "generics", and a good way might be to first try and learn something about the advantages of the design. So I would very much appreciate any pointers to more in-depth information about Swift's generics (or parametric polymorphism) including philosophy/motivation, best practices, how it's implemented (roughly), pros and cons compared to other types of "generics" etc.

Ok you win : ), but I needed the long and detailed explanation given in the accepted answer, and I'm sure I'm not alone. This is like (re)learning how to ride a bike (at age 36) with reversed steering.

Hi Jens,


I'm not sure it's as limiting as you might think, but perhaps you have something in mind that I am not considering.


Completely generic functions that operate on any type cannot do much other than return the values passed as parameters, pass those parameters to other functions, and load and store those parameters.


In Swift you specify up front in the form of conformances what operations a type needs to support in order to be passed as a parameter, e.g.:

protocol Dance {
  func waltz() -> Int
  func jig() -> Int
}

func allTheDances<T: Dance>(dancer: T) {
 dancer.waltz()
 dancer.jig()
}


Now I can call allTheDances with values of any type that conforms to Dance and implements the waltz() and jig() methods. The signature of the function makes it clear that the types that can be passed in must implement those methods.


In a language like C++, you don't need to specify that T conforms to Dance, and the compiler will allow you to compile allTheDances(), but if you try to call allTheDances with a type that does not implement those methods, it will complain when it goes to instantiate the function template with the specific type of the value you pass in (and it needs to do so in order to know to complain).


From the perspective of the user of a function like allTheDances(), it's clearly documented that T must conform to Dance, which I think makes it easier to understand what types can be passed in.


This is especially helpful when you have a chain of calls through generic functions and it's only in the leaf functions that operations other than passing and returning values are done.

Thanks again, although I do feel that I have a pretty clear understanding of protocols and type constraints at least when used as examplified above. I take it that the design of Swift's generics (and especially the IMO surprising case shown by the baz-test) is the way it is because of how it fits together with protocols, protocol extensions, type constraints etc.


I've been writing a lot of code in Swift since the first beta last year and I have kept up to date with the evolution of the language. The only thing that surprised me here is the fact that (looking at my original example) the behaviour of this:

foo(v)

will be totally different depending on, not just the type of v, but whether that type is coming from a type parameter or not, as in:

let v = 111; foo(v)

vs

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

vs

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


That is, I couldn't imagine that the different "values" (of static type information) of the type parameter, which the compiler can see, does not influence the body of the function (eg what foo overload to call inside baz). I thought that the compiler would generate different code for each different T of baz that my code implied/needed. Just as it does for eg

let (a, b) = (111, 1.1)
foo(a)
foo(b)


And as it actually does / have to generate different code for each different T in eg this case (allthough perhaps only as an optimization (and otherwise dynamically(?))), I wonder what percentage of all "somewhat experienced" Swift developers would be able to correctly predict the outcome of that little program in my original post. It still feels like the current behaviour is not the least surprising one ... I guess I will just have to keep digging into this until I really get it.

I completely agree with you, and have been mentioning this for a while.


it is especially limiting when you consider that generic functions are the only way to use generic protocols ( you can't "as?" Cast to them). Dynamic overload resolution would allow you to create data structures which operate only in terms of generic protocol types.


it is extremely odd that, while other types of dispatch are dynamic depending on the runtime type of the object (I.e. the data), overloads depend on how you represented that type in your code at the compile-time. Its not very intuitive. We never had this Objective-C, and this way of doing it doesn't fit with the ultra-dynamic way we're used to working.

Just to make sure we are on the same page: I don't care too much about the "ultra dynamic way" as I never particularly liked that about eg Objective C.


I've never understood the addictedness (esp among Obj-C-only-programmers) to do everything dynamically, even things that could be done much better statically. Ie why give up nice compile time erros, more optimizable code, the possibility of more advanced type system, language and IDE features etc if you don't have to?


So I'm all for making as much as possible known to the compiler (thus static), so that it can give me compile time erros, type inference, smarter optimizations and faster code. Embracing this is one of the, if not the, greatest thing(s) about Swift.


So the questions I raise in this thread is (at least to my mind) all about what the compiler is able to statically resolve (and what it is not able to statically resolve, as in the case of my baz-test). And I think it would be horrible (and utterly incomprehensible) if Swift decided to switch to dynamic-only overload resolution.