Confused by generic behaviour, not clear if it is a bug

I've hit a strange thing that I am finding hard to reason about, and I am not sure whether or not it is a bug I should raise a JIRA for.

In essence, I am trying to create a type that is generic that permits instantiation using a concrete type that may also conform to some other types.

In the platform-neutral sample below you can see the problem. It is not clear why it is possible to instantiate the generic type with ViewType but it is possible with Something, as they seem to represent compatible types.

class View {
    var superview: View?
}

protocol Presenter {
    var viewModel: Any? { get set }
}

class Cache<T> where T: View {
    var data = [AnyHashable: T]()

    func doSomething(instance: T, handler: (T) -> Void) {
        let _ = instance.superview
        handler(instance)
    }
}

/// Usage

class Something: View, Presenter {
    var viewModel: Any?
}

typealias ViewType = View & Presenter

// This works:
let a = Cache<Something>()

// This fails:
//let a = Cache<ViewType>()

let s = Something()
a.doSomething(instance: s, handler: { print($0) })

Right, Something is a concrete type, of which instances can exist.

Right, ViewType is an existential (specifically a subclass existential as allowed by SE-0156). It is not a type, and there is no such thing as “an instance of ViewType” for the same reason there is no such thing as “an instance of Presenter”.

It sounds like you want a type-erased wrapper for “any object whose type inherits from View and conforms to Presenter”. You could define such a type yourself, perhaps calling it “AnyPresenterView”.

• • •

I observe that this is the third thread in the last two days (after this one and this one) asking about protocol existential behavior.

1 Like

Yep... it's a confusing subject and manifests itself in odd places.

Thanks for your clear explanation, it has helped me. However, a type erased wrapper seems a horrible way around this (as usual). Is there really no other solution to this kind of problem that leverages Swift's generics?

No, not as far as I’m aware. The standard library itself uses type-erased wrappers.

Actually, the issue isn't the existential per se, it's the constraint that Cache<T> where T : View. If you remove that constraint, you'll be okay:

class AClass {}
protocol AProto {
  var value: Int { get }
}

class Box<T> {
  var boxed: T
  init(_ b: T) { self.boxed = b }
}

class One: AClass, AProto { var value: Int { return 1 } }
class Two: AClass, AProto { var value: Int { return 2 } }

let a: Box<AClass & AProto> = Box(One())
print(a.boxed.value)
a.boxed = Two()
print(a.boxed.value)

// Output:
// 1
// 2

This is really a problem about metatypes.

I had a little go playing with some workarounds. Nothing worked, but this one shows the real problem:

class AClass {}
protocol AProto {
  var value: Int { get }
}

class Box<T> {
  private var _boxed: T
  init(_ b: T) { _boxed = b }
}
extension Box where T: AClass {
  var boxed: T {
    get { return _boxed }
    set { _boxed = newValue }
  }
}

class One: AClass, AProto { var value: Int { return 1 } }
class Two: AClass, AProto { var value: Int { return 2 } }

let a: Box<AClass & AProto> = Box(One())
print(a.boxed) // error: 'AClass & AProto' is not convertible to 'AnyObject'
a.boxed = Two()
print(a.boxed)

The error message is saying that AClass & AProto is not a subtype of AClass, so we can't access the member in the constrained protocol extension.

Looking at the two examples side-by-side, I get the feeling this could reasonably be called a bug. @Douglas_Gregor?

You would only use a type-erased wrapper when you need this kind of heterogenous storage for an existential with associated types. If you don't, you can use the existential directly. "Protocols in Swift are first-class types" or something.

Well, your example doesn't compile even when stripped down to this:

class AClass {}

class Box<T> {
  private var _boxed: T
  init(_ b: T) { self.boxed = b } // error: 'T' is not a subtype of 'AClass'
}

extension Box where T: AClass {
  var boxed: T {
    get { return _boxed }
    set { _boxed = newValue }
  }
}

let a: Box<AClass> = Box(AClass())

You've got an unconditional initializer init(_:) that tries to assign to _boxed via a computed property boxed that only exists conditionally. I don't think that can be made to compile even in theory. The error is correct in that not all T are subtypes of AClass, and therefore that initializer implementation can't be compiled.

Ah you're right; the initialiser should be setting _boxed directly; that was a typo, and a poor diagnostic: a 'must initialise all stored properties' error would have been more helpful.

I've corrected the failing example, but it still fails at the critical part where you want to use a.boxed, with the same error.

Another interesting point:

let b: AClass & AProto = One()
print(b is AClass) // true
1 Like

Which version of Swift and what is the output from print(...)?

Xcode 9.2 (I think that's Swift 4.1); it's true

Filed: [SR-7008] Subclass existential doesn't satisfy generic class constraints · Issue #49556 · apple/swift · GitHub

I'm almost certain SR-7008 is not a bug but an intended behavior. In your conditional extension the constraint says that T is either AClass or a concrete subclass of it, but the existential AClass & AProto is neither of both. It's not a concrete subclass, this is trivial, and it cannot be AClass because AClass itself does not conform to AProto but the existential requires that.

This is also correct because were checking whether or nor the type of the instance is related to rhs type of the infix is operator. b is AClass does not mean (AClass & AProto) : AClass is true.

Feel free to correct me if I'm wrong here.


As a workaround you can use this constraint:

extension Box where T == AClass & AProto { ... }

@Karl and since you brought me from the other thread, in theory with fixed metatypes you could write the following constraint to allow what you wanted.

class Box<T> where AnyType<T> : AnyType<AClass> { ... }

This should also work for the existential AClass & AProto because their dynamic/static metatype relationships:

AnyType<AClass & AProto> : AnyType<AClass>, AnyType<AProto>

Type<AClass & AProto> : AnyType<AClass>
// AnyType<Any> is implied
// Also notice it does not conform to `AnyType<AClass & AProto>` nor to `AnyType<AProto>`

The static Type relationship becomes obvious when you look at this example:

class S {}
protocol P {}

print("\(type(of: (S & P).self))")      // (S & P).Protocol
print("\(type(of: (S & P).Type.self))") // (S & P).Type.Protocol

The issue is not so much "hey this is actually correct" IMO this is a language usability feature. Unless you understand complex abstract details about existentials this compile-time problem appears to make no sense at all. At the very least much clearer diagnostic is required.

Starting to dip into the Swift language community more I see this pretty much any time a language UX problem is mentioned. Essentially "you're holding it wrong" instead of "Hmm we should look at making this less error prone and easier to understand". I thought that the latter was a pretty important goal of Swift.

The problem here is that these kinds of APIs will be created by people who can understand these details a bit more, but often used by people who do not understand these nuances.

Well diagnostics are written by some people and I think it's fair to say that it's impossible to come up with perfect diagnostics for everything, there are also edge cases that no one thought about. However it is true that the diagnostics here could be better. I have no experience in compiler development, so I cannot contribute here (still on my ToDo-list to dive into that topic). Even I found weird or wrong diagnostics in places where it didn't make any sense. If you don't research intensively then how would you solve issues like SR-631 where you simply expect it to work?!

And if I may recall, the old syntax for APoroto & BProto was protocol<AProto, BProto> which implies that the existential is simply like an anonymous protocol that includes AProto and BProto. And here we're circling back to this thread where I discussed how this issue could be solved if only we had better metatypes. (I'll bring that topic back very soon, I promise.)


A side note for curious people:

The following syntax is a glitch in Swift 4.x which was caused by allowing classes in existentials like AClass & AProto:

protocol SomeName where Self : SomeClass /* glitch */ { ... } 

In the future when this feature is fully supported it will become protocol SomeName : SomeClass { ... }

I'm sharing this because if you understand the old syntax protocol<A, B> then you should be able to get the idea why where Self : SomeClass happend.

To be fair, you specifically asked if it was a bug in both your title and post, so everyone focused on answering that. You can definitely file bug reports asking for clearer error messages whenever you see something that is confusing or misdiagnosed.