Allow `self = x` in class convenience initializers

Huge +1 to this change.

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

2 Likes

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

+1. Have used the hack also!

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

+1, long overdue!!

1 Like

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()
  }
}

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.

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?

1 Like

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???

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 [SR-8713] Protocol extension methods invoked from non-final classes are type-checked incorrectly · Issue #51225 · apple/swift · GitHub.

2 Likes

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

1 Like

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

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

1 Like

Ah okay, got it, thank you for clarification.

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?

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
5 Likes

+1 on this concept.

++1 looks great!

+1

Is there a reason this is only for convenience initializers?

In a non-final class, a designated initializer can be called as part of a super.init delegation from a derived class. The "self" value is going to be a partially initialized instance of the derived class in this case, with the derived class's stored properties already initialized but nothing else. It wouldn't make sense to replace "self" with a fully initialized instance while it is in this state.

10 Likes