Allow `self = x` in class convenience initializers

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.

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

I have a prototype implementation here: https://github.com/apple/swift/pull/19311

11 Likes

I am quite glad there is movement on this! It will definitely help swift-corelibs-foundation be not only more efficient but also work more in-line with the objc implementation for factory style initializers.

Per the designated initializer case it would be nice to allow this if and only if the type of self == the type that was allocated (e.g. a runtime check for the non-final case that Slava brought up). That change would allow SCLF to implement every case that should be a factory pattern.

1 Like

Supporting that would require some fancier control-flow-sensitive analysis than we current do for definite initialization, since we'd have to ensure that a self = assignment only occurred in a context where it was known that Self == ConcreteClass. Maybe if we a type-refining cast feature like like what @grego proposes here we could confine self-assignments in a designated initializer to if <Self == C> condition blocks.

Slightly off-topic, but would it make sense to add a similar feature to Swift like instancetype from Objective-C. That type should bound the currently unconstrained Self to something that you just mentioned Self == C. This is really a missing feature for factory initializer in the context of designated initializers and it would also allow new and safe generic patterns to be expressed in Swift. What do you think @Joe_Groff?

1 Like

Self is instancetype. Factory initializers are interesting in that they do not always return instancetype.

3 Likes

What's the update on this proposal? Anything blocking it?

I haven't had time to finish the implementation. There are still a few issues with the implementation in https://github.com/apple/swift/pull/19311/files that need to be fixed to get the tests passing.

5 Likes

This would be particularly helpful for classes that represent Core Data entities, as it would allow you to define initializers that use NSFetchRequests to retrieve information.

3 Likes

I guess I'm way too impatient, but I'd love to see this feature so I was just wondering when it will be merged :sweat_smile:

2 Likes