0

This is my playground code:

protocol A {
    init(someInt: Int)
}

func direct(a: A) {
    // Doesn't work
   let _ = A.init(someInt: 1)
}

func indirect<T: A>(a: T) {
    // Works
    let _ = T.init(someInt: 1)
}

struct B: A {
    init(someInt: Int) {

    }
}

let a: A = B(someInt: 0)

// Works
direct(a: a)

// Doesn't work
indirect(a: a)

It gives a compile time error when calling method indirect with argument a. So I understand <T: A> means some type that conforms to A. The type of my variable a is A and protocols do not conform to themselfs so ok, I understand the compile time error.

The same applies for the compile time error inside method direct. I understand it, a concrete conforming type needs to inserted.

A compile time also arrises when trying to access a static property in direct.

I am wondering. Are there more differences in the 2 methods that are defined? I understand that I can call initializers and static properties from indirect and I can insert type A directly in direct and respectively, I can not do what the other can do. But is there something I missed?

J. Doe
  • 12,159
  • 9
  • 60
  • 114

1 Answers1

3

The key confusion is that Swift has two concepts that are spelled the same, and so are often ambiguous. One of the is struct T: A {}, which means "T conforms to the protocol A," and the other is var a: A, which means "the type of variable a is the existential of A."

Conforming to a protocol does not change a type. T is still T. It just happens to conform to some rules.

An "existential" is a compiler-generated box the wraps up a protocol. It's necessary because types that conform to a protocol could be different sizes and different memory layouts. The existential is a box that gives anything that conforms to protocol a consistent layout in memory. Existentials and protocols are related, but not the same thing.

Because an existential is a run-time box that might hold any type, there is some indirection involved, and that can introduce a performance impact and prevents certain optimizations.

Another common confusion is understanding what a type parameter means. In a function definition:

func f<T>(param: T) { ... }

This defines a family of functions f<T>() which are created at compile time based on what you pass as the type parameter. For example, when you call this function this way:

f(param: 1)

a new function is created at compile time called f<Int>(). That is a completely different function than f<String>(), or f<[Double]>(). Each one is its own function, and in principle is a complete copy of all the code in f(). (In practice, the optimizer is pretty smart and may eliminate some of that copying. And there are some other subtleties related to things that cross module boundaries. But this is a pretty decent way to think about what is going on.)

Since specialized versions of generic functions are created for each type that is passed, they can in theory be more optimized, since each version of the function will handle exactly one type. The trade-off is that they can add code-bloat. Do not assume "generics are faster than protocols." There are reasons that generics may be faster than protocols, but you have to actually look at the code generation and profile to know in any particular case.

So, walking through your examples:

func direct(a: A) {
    // Doesn't work
   let _ = A.init(someInt: 1)
}

A protocol (A) is just a set of rules that types must conform to. You can't construct "some unknown thing that conforms to those rules." How many bytes of memory would be allocated? What implementations would it provide to the rules?

func indirect<T: A>(a: T) {
    // Works
    let _ = T.init(someInt: 1)
}

In order to call this function, you must pass a type parameter, T, and that type must conform to A. When you call it with a specific type, the compiler will create a new copy of indirect that is specifically designed to work with the T you pass. Since we know that T has a proper init, we know the compiler will be able to write this code when it comes time to do so. But indirect is just a pattern for writing functions. It's not a function itself; not until you give it a T to work with.

let a: A = B(someInt: 0)

// Works
direct(a: a)

a is an existential wrapper around B. direct() expects an existential wrapper, so you can pass it.

// Doesn't work
indirect(a: a)

a is an existential wrapper around B. Existential wrappers do not conform to protocols. They require things that conform to protocols in order to create them (that's why they're called "existentials;" the fact that you created one proves that such a value actually exists). But they don't, themselves, conform to protocols. If they did, then you could do things like what you've tried to do in direct() and say "make a new instance of an existential wrapper without knowing exactly what's inside it." And there's no way to do that. Existential wrappers don't have their own method implementations.

There are cases where an existential could conform to its own protocol. As long as there are no init or static requirements, there actually isn't a problem in principle. But Swift can't currently handle that. Because it can't work for init/static, Swift currently forbids it in all cases.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Ok, so the one compiles down to multiple specialized functions, the other doesn't and one uses a wrapper type. I know some edge cases why protocols can conform to themselves, but I want to know more about those existential wrappers. Quote from the answer 'You can't construct...', why? The compiler needs to know how many bytes it needs, why not ask the wrapper? The type used, is/must in the wrapper right (else it isn't really a wrapper)? When I pass in something that conforms to `A`, it is some conforming type, so I think I should be able to even call an `init` on that type (in my situation). – J. Doe Nov 20 '19 at 21:41
  • I also read cases where it is logical we can not use the `init` method on the type on this question: https://stackoverflow.com/questions/33112559/ from answer https://stackoverflow.com/a/43408193/7715250. I can not see why it shouldn't work in my case though, but maybe that's a compiler limitation. I am wondering, when using the existential wrapper, does the type gets lost somehow (it happens in Rust I thought)? Else I won't see a reason why my code isn't compiling, setting aside compiler limitations. It is guaranteed that in both methods an instance of a conforming type `A` is passed. – J. Doe Nov 20 '19 at 21:43
  • I'm not sure what you mean by "ask the wrapper" in order to construct an object. The wrapper is *around* some other object. It doesn't have its own implementations of any of the methods; what would it return if the protocol required `var peripheral: CBPeripheral { get set }`. (I picked CBPeripheral as an example because there is no public init for it.) You create an existential by passing it a constructed, existing object. – Rob Napier Nov 20 '19 at 22:11
  • While it's passed a conforming instance, it also is being passed a type, and you could, as you did in your example, ignore the instance and call `A.init` on that type. As I noted, there are cases where it would be possible (if you didn't require `init` on the protocol), and there has been discussion of allowing that case. The main concern has been that it makes protocols with init/static in them very different from other protocols, in the same way that associated types make protocols very different. There's been some concern about creating a third version of protocols w/ different abilities. – Rob Napier Nov 20 '19 at 22:16
  • There's also been discussion about allowing direct construction of existentials if all the requirements had default implementations, or perhaps designating some concrete type the default implementation of the protocol. And that may be possible, but again, it creates more complexity around an already very complicated system, so I don't know if that will ever happen (I tend to doubt it). – Rob Napier Nov 20 '19 at 22:20
  • Well the answer states that Swift doesn't know how many bytes it must allocate for a given `A` ("some unknown thing that conforms to those rules") but a wrapper is inserted right? I thought it could be possible to look inside the type of the wrapper what thing is inserted, but ok I understand there are some limitations – J. Doe Nov 21 '19 at 08:22