It is impossible to correctly implement Copyable (returning Self)

Originator:robnapier
Number:rdar://23454180 Date Originated:07-Nov-2015 05:17 PM
Status:Open Resolved:
Product:Developer Tools Product Version:Xcode 7.2 Beta (7C46t)
Classification:Enhancement Reproducible:Not Applicable
 
It is currently impossible to correctly implement the following protocol (or any protocol that approximates it) in a way that does not lead to constant surprise in the face of subclassing.

protocol Copyable {
    func copy() -> Self
}

This would seem to be about the simplest and most obvious protocol that involves Self, but it is an eternal maze of twisty passages the moment subclasses enter the picture, and Swift provides no mechanism for excluding them. (There is no mirror of “class” protocols that demand “noclass.”)

This particular version of Copyable is not possible to implement at all. There’s no way to construct the required object:

class A: Copyable {
    var x: Int
    init(x: Int) { self.x = x }
    func copy() -> Self {
        return A(x: x)                    // Cannot convert return expression of type 'A' to return type 'Self'
        return Self(x: x)                  // Use of unresolved identifier 'Self'
        return self.dynamicType.init(x: x) // Constructing an object of class type 'Self' with a metatype value must use a 'required' initializer
    }
}

This is reasonable given Swift’s strict construction requirements. The problem is when you try to move forward with a required copying init: 

protocol Copyable {
    init(copying: Self)
}

extension Copyable {
    func copy() -> Self {
        return self.dynamicType.init(copying: self) // This resolves to init(copy: A), never init(copy: B)
    }
}

class A: Copyable {
    var x: Int
    init(x: Int) { self.x = x }
    required init(copying other: A) { x = other.x }
}

class B: A {
    var y: Int
    init(x: Int, y: Int) {
        self.y = y
        super.init(x: x)
    }
    required init(copying other: A) {
        print("B.init(copy:A)")
        y = 0
        super.init(copying: other)
    }
    required init(copying other: B) { // "required" not required here, but it should be according to the protocol, and is ignored by Swift.
        print("B.init(copy:B)")
        y = other.y
        super.init(copying: other)
    }
}

let b = B(x: 1, y: 2)

let c = b.copy()
(c.x, c.y)  // (1, 0)! So that's still not right.

let c2 = B(copying: b)
(c2.x, c2.y)  // (1, 2), works, but only if b is exactly type B, and not a subclass.

let c3 = b.dynamicType.init(copying: b)
(c3.x, c3.y)  // (1, 2), works when done by hand, but lacks enforcement

The main issue is that the protocol requires init(copying: Self), but B isn’t required to implement init(copying: B). While B can certainly be passed to A, init(copying: B) is not the same type as init(copying: A), and so shouldn’t be sufficient to conform to the protocol. If I replace A’s init(copying: A) with init(copying: Any), that’s not sufficient for A to conform. By the same reasoning, B shouldn’t conform without implementing copying:B.

The problem is deeper, however. Even though I *do* implement copying:B, it is never called in any code where the type is generic. For example:

extension Copyable {
    func copy() -> Self {
        return self.dynamicType.init(copying: self) // This resolves to init(copy: A), never init(copy: B)
    }
}

When called as “b.copy()”, Self resolves to B. However, init(copying:A) is called rather than init(copying:B). This is extremely surprising to the caller. In a generic function, the same problem occurs:

func copy<T: Copyable>(x: T) -> T {
    return x.dynamicType.init(copying: x)
}
func bcopy(x: B) -> B {
    return x.dynamicType.init(copying: x)
}

let c4 = copy(b)
(c4.x, c4.y)  // (1, 0)! So that's still not right.

let c5 = bcopy(b)
(c5.x, c5.y) // (1,2) works


One might argue that, for type B, the correct answer is to copy with B(copying:b), but that’s not sufficient to correctly copy. If there is a subclass of B, I may not even know the actual type of b. The only semi-reliable solution is to call b.dynamicType.init(copying: b), though the compiler doesn’t ensure that the subclass correctly overloads the copying init (you’re only required to implement copying:A).

Ultimately this is worse than the situation with NSCopying, which drives us back to AnyObject and “x.copy() as! X” which is dangerous and abandons exactly the safety that Swift’s strict (and sometimes tedious) constructors were supposed to provide.

Ultimately we need some way to express “let x = y.copy()” such that the resulting x has the same type as y and is promised to be fully initialized. If it is not possible, then Self should be forbidden in non-final classes to avoid the current surprising behavior. Everything looks fine, and seems to work, until suddenly the wrong init() is called for a subclass you didn’t know about during testing.

Comments

Wrong parameter name

The comment inside copy for the Copyable extension should read: // This resolves to init(copying: A), never init(copying: B) It's also wrong in all the print statements. I assume you've refactored your protocol and all the strings/comments are still with the old init(copy: Self) variant.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!