Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can not conform to protocol by creating extension with Where Clauses

Tags:

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel: Typographable {}

extension Typographable where Self == UILabel {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

I have create a protocol Typographable, the UILabel implement this protocol, and the implementation is in the extension Typographable where Self == UILabel.

it works perfectly in swift 4.0, but it not works any more in swift 4.1, the error message is Type 'UILabel' does not conform to protocol 'Typographable'

I have read carefully the CHANGELOG of swift 4.1, but I can't find anything useful.

Is this normal, did I miss something ?

like image 464
JIE WANG Avatar asked Apr 12 '18 09:04

JIE WANG


1 Answers

This is pretty interesting. Long story short (okay maybe not that short) – it's an intentional side effect of #12174, which allows for protocol extension methods that return Self to satisfy protocol requirements for non-final classes, meaning that you can now say this in 4.1:

protocol P {
  init()
  static func f() -> Self
}

extension P {
  static func f() -> Self {
    return self.init()
  }
}

class C : P {
  required init() {}
}

In Swift 4.0.3, you would get a confusing error on the extension implementation of f() saying:

Method 'f()' in non-final class 'C' must return Self to conform to protocol 'P'

How does this apply to your example? Well, consider this somewhat similar example:

class C {}
class D : C {}

protocol P {
  func copy() -> Self
}

extension P where Self == C {
  func copy() -> C {
    return C()
  }
}

extension C : P {}

let d: P = D()
print(d.copy()) // C (assuming we could actually compile it)

If Swift allowed the protocol extension's implementation of copy() to satisfy the requirement, we'd construct C instances even when called on a D instance, breaking the protocol contract. Therefore Swift 4.1 makes the conformance illegal (in order to make the conformance in the first example legal), and it does this whether or not there are Self returns in play.

What we actually want to express with the extension is that Self must be, or inherit from C, which forces us to consider the case when a subclass is using the conformance.

In your example, that would look like this:

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel: Typographable {}

extension Typographable where Self : UILabel {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

which, as Martin says, compiles just fine in Swift 4.1. Although as Martin also says, this can be re-written in a much more straightforward manner:

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel : Typographable {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

In slightly more technical detail, what #12174 does is allow the propagation of the implicit Self parameter through witness (conforming implementation) thunks. It does this by adding a generic placeholder to that thunk constrained to the conforming class.

So for a conformance like this:

class C {}

protocol P {
  func foo()
}

extension P {
  func foo() {}
}

extension C : P {}

In Swift 4.0.3, C's protocol witness table (I have a little ramble on PWTs here that might be useful in understanding them) contains an entry to a thunk that has the signature:

(C) -> Void

(note that in the ramble I link to, I skip over the detail of there being thunks and just say the PWT contains an entry to the implementation used to satisfy the requirement. The semantics are, for the most part, the same though)

However in Swift 4.1, the thunk's signature now looks like this:

<Self : C>(Self) -> Void

Why? Because this allows us to propagate type information for Self, allowing us to preserve the dynamic type of the instance to construct in the first example (and so make it legal).

Now, for an extension that looks like this:

extension P where Self == C {
  func foo() {}
}

there's a mismatch with the signature of the extension implementation, (C) -> Void, and the signature of the thunk, <Self : C>(Self) -> Void. So the compiler rejects the conformance (arguably this is too stringent as Self is a subtype of C and we could apply contravariance here, but it's the current behaviour).

If however, we have the extension:

extension P where Self : C {
  func foo() {}
}

everything's fine again, as both signatures are now <Self : C>(Self) -> Void.

One interesting thing to note about #12174 though is that is preserves the old thunk signatures when the requirements contain associated types. So this works:

class C {}

protocol P {
  associatedtype T
  func foo() -> T
}

extension P where Self == C {
  func foo() {} // T is inferred to be Void for C.
}

extension C : P {}

But you probably shouldn't resort to such horrific workarounds. Just change the protocol extension constraint to where Self : C.

like image 161
Hamish Avatar answered Sep 28 '22 19:09

Hamish