2

Can someone explain why is passing protocol var to a generic function an error in Swift?

protocol P {}
func f<T: P>(_: T) {}
func g(x: P) { f(x) } // Error

This, however, is not an error:

protocol P {}
func f(_: P) {}
func g(x: P) { f(x) }

I was just wondering what is the difference of the code generated by the compiler which makes it to reject the first example but in second case the generated code is good to go. Both seem to give the behavior I would expect.

frangulyan
  • 3,580
  • 4
  • 36
  • 64
  • 1
    [Protocols don't conform to themselves](https://stackoverflow.com/a/43408193/2976878), so `P` cannot satisfy the generic placeholder `T` where `T : P`. – Hamish Sep 30 '17 at 14:34
  • @Hamish I edited it, please review to check if this is still a duplicate. In the second example I am passing protocol P to a function expecting P, so "P don't conform P" explanation is not valid, otherwise it should've given an error also. – frangulyan Sep 30 '17 at 14:51
  • Hmm; not sure if still an *exact* duplicate, although the explanation given in the linked Q&A is that you cannot satisfy the placeholder `T` with `P` when `T` is constrained to `P`. In `func g(x: P) { ... }`, there is no constrained placeholder, so no problem. So the explanation given is still valid. A parameter typed as `P` takes *any* `P` instance, a parameter of type generic placeholder `T` where `T : P` takes a *specific* concrete type that conforms to `P`; which `P` itself is not. – Hamish Sep 30 '17 at 14:55
  • So seems like an implementation limitation. A guy here ( http://www.russbishop.net/swift-associated-types-cont ) wrote that "Even if we can prove an existential satisfies constraints we can't call the correct generic specialization dynamically.". I understood him so - since generic f will result in bunch of f's when compiled, the compiler doesn't know which one of those to call. – frangulyan Sep 30 '17 at 15:00
  • It's an implementation limitation for protocols that *don't have static requirements*, yes (I do discuss all of this in my answer here btw: https://stackoverflow.com/a/43408193/2976878). It's a type safety limitation in cases where the protocol does have static reqs. "*since generic f will result in bunch of f's when compiled*" – not necessarily. The compiler doesn't by default generate individual specialised implementations of a generic function when compiled (only as an optimisation, and only when it has access to the original implementation; for example it cannot do it across modules). – Hamish Sep 30 '17 at 15:03
  • I see, thanks. I was just wondering what is the difference of the code generated by the compiler which makes it to reject the first example but in second case the generated code is good to go. My "high-level" idea was that in case of generics it wants to do some extra compile-time stuff (hence needs to know exact classes) while in non-generic version it will just go to old "runtime polymorphism". Anyway, thanks for your explanation! – frangulyan Sep 30 '17 at 15:09
  • 1
    Well the main difference is that with the generic version (assuming the implementation isn't specialised), the function takes an extra implicit parameter for the metatype value for `T`; and obviously this metatype must represent a type that satisfies the constraints on `T`, so we cannot pass `P.self` for it as `P` does not (yet) conform to `P`. – Hamish Sep 30 '17 at 15:36
  • 1
    Bear in mind that in the implementation of the generic function the compiler uses that metatype value for the generic placeholder in order to access the value witness table of the type (which details how to load, deallocate, etc. the opaque (indirect) memory passed as a `T` typed argument). If `T` were `P`, the function wouldn't be able to do these operations, as it doesn't know the underlying concrete type of the instance. For example, how would it perform a copy (e.g say you did `var t = t; t.someMutatingMethod()`)? Glad my explanation(s) were useful :) – Hamish Sep 30 '17 at 15:36
  • 1
    (Protocol-typed instances are slightly different as the value witness table for the concrete type is passed about with the instance itself). Basically, there's lots of complexity to this issue which ultimately makes it non-trivial for the Swift team to remove the restriction for protocols without static requirements. Although hopefully they will do in a future version of the language :) – Hamish Sep 30 '17 at 15:38
  • 1
    Actually now that I think about it, I guess I should maybe write the above up as an answer to your question and re-open? Seems like too much content for comments. – Hamish Sep 30 '17 at 15:41
  • Yes, would be great! And useful for others, hopefully – frangulyan Sep 30 '17 at 17:27
  • I think I slowly start to understand the conceptual difference of the 2 examples, just needed explanation on a very high level to "feel it". In the generic case I am introducing a new type into the world. And this new type lives it's own life - it can create variables, copy objects and so on, as you mentioned. For all this we need more than just an interface, that's why we need to know how to handle it's memory (we need to pass metatype, as you said). And in the second case I'm only going to manipulate the given object by its known interface. Now it starts to make sense! – frangulyan Sep 30 '17 at 17:46
  • Awesome; I didn't get a chance to write up an answer today, but should be able to do so tomorrow. I'll reopen the question in case anyone else wants to chime in. Btw, it would seem to me that the main question here that isn't answered by the linked Q&A (also [this Q&A](https://stackoverflow.com/q/38446487/2976878) is probably relevant) is "*I was just wondering what is the difference of the code generated by the compiler which makes it to reject the first example but in second case the generated code is good to go*". Might be worth putting in the main question? – Hamish Oct 01 '17 at 00:57
  • My apologies for taking so long to get back to this! I've posted an answer, and am happy to take any more questions :) – Hamish Jun 01 '18 at 22:02

1 Answers1

3

Can someone explain why is passing protocol var to a generic function an error in Swift?

protocol P {}
func f<T: P>(_: T) {}
func g(x: P) { f(x) } // Error

It’s because currently non-@objc protocols don't conform to themselves. Therefore P cannot satisfy the generic placeholder T : P, as P is not a type that conforms to itself.

However in this particular example, that is, one where P doesn't have any static requirements, there's no fundamental limitation preventing P from conforming to itself (I explain this in more detail in the above linked Q&A). It's merely an implementation limitation.

What is the difference of the code generated by the compiler which makes it to reject the first example but in second case the generated code is good to go

Protocol-typed values (existentials) are implemented in a slightly different manner to generic-typed values constrained to a protocol.

A protocol-typed value P consists of:

  • An inline value buffer for the stored conforming value (currently 3 words in length, but is subject to change until ABI stability). If the value to store is more than 3 words in length, it's put into a heap allocated box, and a reference to this box is stored in the buffer.
  • A pointer to the conforming type's metadata.
  • A pointer to the protocol witness table for the conformance of the value to P, which lists the implementations to call for each of the protocol requirements.

On the flip side, a generic-typed value T where T : P consists of only the value buffer. The type metadata and witness table(s) are instead passed as implicit arguments to the generic function, and any member accesses or memory manipulations for values of type T can be done by consulting these arguments. Why? Because Swift's generics system ensures that two values of type T must be of the same type, so they must share the same conformance to the protocol constraint(s).

However this guarantee breaks down if we allow protocols to conform to themselves. Now, if T is a protocol type P, two values of type T could potentially have different underlying concrete types and therefore different conformances to P (so different protocol witness tables). We'd need to consult protocol witness tables on a per-value (rather than per-type) basis – just like we do with existentials.

So what we'd want is for generic-typed values to have the same layout as an existential of the protocol constraints. However this would make things pretty inefficient for the vast majority of the cases when the generic placeholder is not being satisfied by a protocol type, as values of type T would be carrying about redundant information.

The reason why @objc protocols are allowed to conform to themselves when they don't have static requirements is because they have a much simpler layout than non-@objc existentials – they just consist of a reference to the class instance, where protocol requirements are dispatched to via objc_msgSend. This layout is shared with that of a value typed as a placeholder T constrained to the protocol, which is why it's supported.

Hamish
  • 78,605
  • 19
  • 187
  • 280