Allow `self = x` in class convenience initializers


(Joe Groff) #1

In https://github.com/apple/swift/pull/19151, I'm changing code generation for class convenience initializers so that they only have "allocating" entry points, which are responsible for allocating and initializing the entire object. (See Changing class convenience initializers to perform whole object allocation and @objc interop for more background.) With this change, convenience initializers are nearly identical to value or protocol extension initializers at the implementation level, since the self.init delegation within a convenience initializer is effectively initializing the self reference with a reference to the instance created by the self.init call rather than initializing the instance itself. At this point, it's not that big of a leap to allow the self reference to also be initialized by assignment, as it can be in value and protocol extension initializers, like this:

class Y {
  required init() {}
  static func singletonInstance() -> Self { return self.init() }
  convenience init(pref: Y) {
    self = type(of: self).singletonInstance()
  }
}

This opens the door to convenience initializers being able to use static factory methods or other Self-returning operations to produce their returned result, not only the result of other initializers. Swift's own standard library and Foundation overlay hack around this missing functionality by making classes conform to dummy protocols and using protocol extension initializers where necessary to implement this functionality:

protocol P {
  static func instance() -> Self
}

extension P {
  init() {
    self = Self.instance()
  }
}

class C: P {
  static func instance() -> Self { ... }

  convenience init(x: ()) {
    self.init()
  }
}

Because a class convenience initializer can already delegate to a protocol extension initializer, and a protocol extension initializer can produce an arbitrary class instance of Self type when invoked on a conforming class (including potentially a subclass of the covariant Self it was invoked on), I don't think there's any weakening of the invariants one can expect from an arbitrary convenience initializer—you already can't assume without further analysis that an initializer creates a fresh object or that the returned object has exactly the dynamic class type the initializer was invoked on. This seems like a small improvement that could eliminate some hacky workarounds in our and other people's codebases. What do you all think?


(Tanner) #2

Huge +1 to this change.

I maintain code that uses hacks like this as well and I would love to get rid of it.


(Rod Brown) #3

:+1: from me! I actually ran into this limitation yesterday.


(Howard Lovatt) #4

+1. Have used the hack also!


(Pranshu Goyal) #5

+1 from my side, waiting for this since a long time.


(Chris Lattner) #6

+1, long overdue!!


(Adrian Zubarev) #7

Is this a typo in the example, pref is never used?! Otherwise I don't understand the purpose of this example except that it shows an unambiguous init(pref:) as a factory init.

Other than that, Swift still does not allow the usage of Self in classes (SE-0068 is still not implemented after years). In the given example I read Self as it would mean the same thing like if you had a protocol with Self that this class conforms to. I had a short discussion on twitter [1] with @Slava_Pestov about the proposal a while ago. I must admit that I did not fully understand the technical issue with Self, but he said that we have to reconsider what Self would mean on classes. It seems that the reconsideration of Self touches this pitch since we need to clarify what the following code means:

extension Y {
  convenience init(other: Self) {
    self = other
  }
}

Or what does Self means in the related thread from the [1] discussion in case of a generic type parameter:


Nevertheless I really like the general sense of this pitch especially if combined with SE-0068. Then I can finally refactor this pattern to something more naturally hidden:

// Before
protocol XibDesignable : AnyObject {}

extension XibDesignable where Self : UIView {
  static func instantiateFromXib() -> Self {
    let metatype = Self.self
    let bundle = Bundle(for: metatype)
    let nib = UINib(nibName: "\(metatype)", bundle: bundle)
    guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
      fatalError("Could not load view from nib file.")
    }
    return view
  }
}

// After:
extension UIView {
  static func instantiateFromXib() -> Self {
    let metatype = Self.self
    let bundle = Bundle(for: metatype)
    let nib = UINib(nibName: "\(metatype)", bundle: bundle)
    guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
      fatalError("Could not load view from nib file.")
    }
    return view
  }
}

final class View : UIView {
  convenience init() {
    self = Self.instantiateFromXib()
  }
}

(Slava Pestov) #8

I think the best interpretation of SE-0068 is that Self in a class is the dynamic type of self, so your above example will be invalid. However, it's also not a useful thing to define. You'll never call Y(other: foo) directly, instead just referring to foo; and you'll never delegate to Y.init(other:) via self.init, since you can instead write self = other.


(Adrian Zubarev) #9

The invalid part is that Y is not while final other can be a subclass Z of Y where self = other would mean 'assign an instance of Z to a storage of Y' which cannot/shouldn't happen. Is that what you mean, more or less? So a better solution would be to come up with another way to refer the current type:

class A {
  init() { ... }
  init(other: InstanceSelf) { ... }
}

class B : A { ... }
let b = B()
let bb = B(other: b) // Valid since `InstanceSelf == B`

In that sense though, why do we need a restriction for factory init's to be convenience only?


#10

As per the automatic initializer inheritance rule:

Rule 1
If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.
Rule 2
If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.

Simply allowing to assign to self have a problem:

class Animal {
  init() {}
  convenience init(with instance: Animal) {
    self = instance
  }
}

class Cat : Animal {
  override init() {}
}
class Dog : Animal {
  override init() {}
}

let cat: Cat = Cat(with: Dog()) // Dog with Cat type???

Although we already have this problem with the current "hackish" factory initializer with protocol, if we allow assigning to self, I think we should amend Rule 2 to exclude "assigning to self" convenience initializers. i.e. those initializers should never be inherited.

Hackish factory initializer example which compiles in 4.2
protocol P {}
extension P {
  init(_with instance: Self) {
    self = instance
  }
}
class Animal : P {
  init() {}
  convenience init(with instance: Animal) {
    self.init(_with: instance)
  }
}
class Cat : Animal {
  override init() {}
}
class Dog : Animal {
  override init() {}
}

let cat: Cat = Cat(with: Dog()) // Dog with Cat type???

(Joe Groff) #11

self has Self type, not Animal type, so the type system still prevents you from writing something unsound by the normal convenience initializer inheritance rules, and we shouldn't change the rules because of an implementation detail of the body. Protocol initializers must produce Self as well. As @DevAndArtist noted, if we allowed references to Self as a type inside method bodies, you could say self = instance as! Self to explicitly force an initializer that only works dynamically when Self == Animal if that's what you want. It looks like a bug that this is accepted:

class Animal : P {
  init() {}
  convenience init(with instance: Animal) {
    self.init(_with: instance)
  }
}

The protocol extension method with contravariant Self argument ought to be invoked with the protocol's Self bound to the dynamic Self of the class, not to Animal, in which case self.init(_with: instance) should not type check because the instance argument is not of Self type. This ought to be an error. I filed https://bugs.swift.org/browse/SR-8713.


(Joe Groff) #12

Yes, that was just a typo, sorry. The only purpose of the argument there is to disambiguate the initializers.


(Adrian Zubarev) #13

So it should be type(of: pref).singletonInstance() I guess?


(Joe Groff) #14

As you noted, self must still be assigned something of Self type, so type(of: self) is what I meant.


(Adrian Zubarev) #15

Ah okay, got it, thank you for clarification.


(Mox) #16

As pointed out by @ahti in Workarounds for Self in classes, for static methods self is equivalent to self.Type. So your example above can be simpler:

// After:
extension UIView {
  static func instantiateFromXib() -> Self {
    let bundle = Bundle(for: self)
    let nib = UINib(nibName: "\(self)", bundle: bundle)
    guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
      fatalError("Could not load view from nib file.")
    }
    return view
  }
}

However, what is missing is coercing to Self. Neither as? self nor as? Self is ok in classes, but latter works fine in protocols. On the other hand, is there a way to workaround this if one could assign to self? I guess there would still be type mismatch?


(Slava Pestov) #17

I actually fixed this already, but staged it in with -swift-version 5 since it's source breaking (Foundation depends on the unsafe behavior). If you build with -swift-version 5 you will see the type checker rejects your protocol extension example:

$ ../build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/bin/swiftc a.swift -swift-version 5
a.swift:10:22: error: cannot convert value of type 'Animal' to expected argument type 'Self'
    self.init(_with: instance)
                     ^~~~~~~~
                              as! Self

(Joe Spadafora) #18

+1 on this concept.


(アンドレカンドレ) #19

++1 looks great!


(Jon Hull) #20

+1

Is there a reason this is only for convenience initializers?