Issue with typealias of multiple protocols

This is the problematic code:

public protocol Blah {
    func blah(id: String) -> String
}

public protocol Blih {
    func blih(id: String) -> String
}


typealias DataSource = Blah & Blih

struct MyDataSource: DataSource {
    func blah(id: String) -> String {
        return "blah " + id
    }

    func blih(id: String) -> String {
        return "blih " + id
    }

}

func something<DS: Blah>(id: String, b: DS) {
    print(b.blah(id: id))
}

something(id: "foo", b: MyDataSource())


struct Wrapper<DS, P> {
    var value: DS
    var val2: P
}

func other<DS, P>(id: String, w: Wrapper<DS, P>) where DS: Blah {
    print(w.value.blah(id: id))
}

let wrapped = Wrapper<DataSource, String>(value: MyDataSource(), val2: "Ignored")

other(id: "bar", w: wrapped)

And this is the error that Xcode gives:

error: Issue.playground:104:21: error: in argument type 'Wrapper<DataSource, String>' (aka 'Wrapper<Blah & Blih, String>'),
'DataSource' (aka 'Blah & Blih') does not conform to expected type 'Blah' 
other(id: "bar", w: wrapped)
                ^

I’m not sure if this is a compiler error, or I’m just doing something wrong here.

You struct Wrapper does not conform to Datasource itself. The generic property of wrapper does conform to the DataSource-Protocol.

change

other(id: "bar", w: wrapped)

to

other(id: "bar", w: wrapped.value)

That should fix your problem.

Sorry, no it doesn’t. Function other is expecting a Wrapper<Blah, String>, and I’m providing a Wrapper<DataSource, String>. The problem is the casting from DataSource to Blah. When there’s no wrapper it works fine, as you can see in the something function. You can see that the error doesn’t make much sense:

'DataSource' (aka 'Blah & Blih') does not conform to expected type 'Blah'

Sorry, I payed not enough attention and didn’t see the constraint within the method signature…

Removing the generic specification will make the code working.
Changing

 let wrapped = Wrapper<DataSource, String>(value: MyDataSource(), val2: "Ignored")

to

  let wrapped = Wrapper(value: MyDataSource(), val2: "Ignored")

will do the trick.

I’m a bit curious, why this happens. I’ve got an idea, but I want to check that, when I’ve the time to do so. I will reply here later.

I believe the right explanation to this is that DataSource or Blah & Blih itself does not conform to Blah or Blih. Rather, being an existential, it is a type that conforms to both Blah and Blih. So, the correct way to write it is:

let wrapped = Wrapper<MyDataSource, String>(value: MyDataSource(), val2: "Ignored")

other(id: "bar", w: wrapped)

or leave the generics implicit as @Maik639 suggested, which will produce the same result.

The problem with @Maik639 solution is that it forces me to pass around a specific type. I would really want to pass around instances of Wrapper<DataSource, String>, instead of forcing it to be Wrapper<MyDataSource, String>.

Also, please bear in mind that if we remove the wrapper everything works, as seen in the something function, so it’s this double indirection that is bugging the compiler I guess.

This is because invoking something like this something(id: "foo", b: MyDataSource()) generates something<MyDataSource>.

Apart from that, I see what you mean. This is happening due to the fact that Swift generics don’t support constrained parameters to be protocols:

protocol P {}

protocol P1: P {}

class A: P1 {}

let a: P1 = A()

func foo<T: P>(arg: T) {}

foo(arg: a) // Error

Your example is analogous, but slightly more complex, which is most likely the reason the compiler spits a senseless error.

1 Like

The problem is really in the line

let wrapped = Wrapper<DataSource, String>(value: MyDataSource(), val2: "Ignored")

to which the easiest solution is to let Swift just infer the generics (as others have mentioned).

To see why it’s a problem I think the following example makes it a bit easier to understand the error message you’re getting:

error: in argument type 'Wrapper<DataSource, String>' (aka 'Wrapper<Blah & Blih, String>'),
'DataSource' (aka 'Blah & Blih') does not conform to expected type 'Blah' 

Consider:

protocol Foo {
    func foo() -> Bool
    init()
}
protocol Bar {
    func bar() -> Bool
    init()
}

typealias FooBar = Foo & Bar

struct Concrete: FooBar {
    func foo() -> Bool { return false }
    func bar() -> Bool { return true }

    init() {}
}

func create<B>(type: B.Type) -> B where B: Foo {
    return B()
}

let a = create(type: Concrete.self)
let b = create(type: FooBar.self)

Here the create(type:) function is essentially your other(id:w:). It takes different arguments, however the situation with the generics is the same. I’ve used meta-types here because Swift needs something to deduce the generic parameter, but we don’t actually use the argument in the function so really any method of specifying the type will yield the same result.

The line let a = create(type: Concrete.self) succeeds and returns an instance of Concrete. However let b = create(type: FooBar.self) does not:

error: in argument type 'FooBar.Protocol' (aka '(Bar & Foo).Protocol'), 'FooBar' (aka 'Bar & Foo') does not conform to expected type 'Foo'
let b = create(type: FooBar.self)
                     ^

If we were permitted to write the generic parameters for functions explicitly, it is easy to see that the above call would be identical to:

let b = create<FooBar>(type: FooBar.self)

We can then see what the problem would be if Swift allowed the function call: when the generic parameter is FooBar, which B do we mean if we use the type in the function body?

i.e. what would return Foobar() mean?

The example is contrived, in that init() is actually part of the interface of Foo (which constrains the generic parameter), so we know how to instantiate any given Foo. But is is hopefully clear that when passing a protocol FooBar, there are cases that can arise when Swift doesn’t know which implementation of it to use.

In your example, when you define

let wrapped = Wrapper<DataSource, String>(value: MyDataSource(), val2: "Ignored")

And then call

other(id: "bar", w: wrapped)

If we were to write the generic parameters explicitly, since wrapped is of type Wrapper<DataSource, String>, this would be

other<DataSource, String>(id: "bar", w: wrapped)

And so this is the same situation as before, a protocol has been used in a constrained generic parameter – which implementation would we use if we referred to it directly?

I think that in theory your particular usage would not run into the problem demonstrated because your protocols do not have static requirements (init acts like a static requirement because you’d use it by referring to the type), which means that all requirements you could possibly use require an instance (which would always have a concrete implementation).

I think that this could potentially be something that could be relaxed as a restriction (i.e. allow this behaviour when protocols only have instance requirements), some very careful consideration would need to be done though.

Hopefully this at least illustrates why, in general, if you place constraints on generics then it might allow problems to arise when you try to use a protocol (without a concrete implementation) as a type inside the function (hence why the error exists).

4 Likes

Very accurate. It is obvious now why does namely this error arise - the compiler strictly expects a non-protocol type as a constrained generic parameter. That said, a protocol can’t conform to anything, which is exactly what the error says: type SomeProtocol does not conform to OtherProtocol. This thread helped to understand once more the reason why @DevAndArtist is concerned about metatypes and the flexibility they could introduce if refactored.

@DevAndArtist Looking forward to brainstorm this again

Circling back to some misunderstood issues in this thread:

A quick sum up for existentials:

  • A & B is called an existential
  • Existentials had a different syntax before Swift 3, it was protocol<A, B>
  • Before Swift 4 existentials could only be composed from protocols (now a class constraint or a specific class is also allowed)
  • You can think of the existentials more or less like anonymous protocols which your type will conform to if the type is a subtype of all types the existentials is composed of (this is only a theoretical explanation and probably not the way the compiler handles it) - very similar to the behaviour in Go (golang)

Some infos about metatypes:

  • SomeType.self will return a static metatype of type SomeType.Type
  • SomeProtocol.self will also return a static metatype but this time the type is SomeProtocol.Protocol
  • There are also dynamic matetypes which reuses the .Type syntax
  • Static and dynamic metatypes seems to be merged in the current Swift
  • Static metatypes of concrete types are subtypes of the it’s dynamic metatype T.Type : T.Type (if T is a concrete type)
  • Static metatype of concrete protocols and exisitentials (remember the old notation protocol<...>) are not subtypes of their dynamic metatype. P.Protocol : P.Type is wrong, but P.Protocol : Any.Type is still valid.
  • The protocol metatype P.Type does also exist but there is no way to get such an instance and use it for casts
  • We’re not allowed to write P.Protocol anywhere in our code, only P.Type
  • In generic context and associated type context the assumption of (static) T.Type : (dynamic) T.Type breaks if you pass a concrete protocol or an existential (this is the issue from this thread) because now you’re working with the P.Protocol thing which is not a subtype of P.Type

If we could separate static and dynamic metatypes, then we could make the described difference between concrete types and protocols / existentials crystal clear for everyone, but also provide new generic constraints which would allow you do the exactly the thing that you initially wanted. Maybe we could even rotate the meaning of T : P so that the new constraints (AnyType<T> : AnyType<P>) become unnecessary (but this is only wish thinking of mine).

If you’re interested in my concerns about metatypes, I highly encourage you to read the discussion thread I started and maybe the topics and the proposal I linked.

I thought that A as well was an existential? Like if A doesn’t have Self requirements, then [A] would contain existentials and not concrete types as these could have different memory layouts.

Well, obviously, if protocols are not allowed, existentials all the more aren’t either

class A {}
var something: A 
               ^ this is an existential too

protocol P {
  associatedtype Q
}

struct S : P {
  typealias Q = Int
                ^ concrete existential
}

That is the only area where I have to admit that I might get confused as well. I’d like to keep it simple and I only refer the combined existentials as existentials even if there are a few more.


Again that is more theory than the reality on how the Swift compiler handles it, there are other mechanisms that are responsible for that.

A can be an existential, i.e

protocol A {}
protocol B {}

protocol C {}

typealias P = A & B

typealias PP = P & C

But if it’s a simple protocol, it is not an existential as far as I know

For more information about existentials you can look up the wiki article.