Un-requiring required initializers

In Swift, although we try to provide a sound, total type system as the default environment for the most part, we provide reasonably easy explicit ways to poke holes where the type system isn't a perfect fit, such as ! for unwrapping optionals known to never be nil, as! for casting to a type known to match, and try! for ending error propagation when errors are known to never to be possible. However, one place we don't provide a good type system escape hatch is with class initializers and inheritance. If a class conforms to a protocol, all of its subclasses must in order to be substitutable. If that protocol requires initializers, that initializer implementation therefore must be required of every subclass. This is necessary for the soundness of the type system, but oftentimes it doesn't match the reality of the API expectations. In the best case, this mismatch leads to boilerplate trap implementations; for example, UIView conforms to NSCoding in order to be usable with Interface Builder, but in practice user-defined views are often never used in nibs or storyboards; these view classes nonetheless must declare a stub implementation of init(coder:):

class MyView: UIView {
  required init(coder: NSCoder) {
    fatalError("i don't want this and will never call it")
  }
}

In the worst case, this severely limits the expressivity of non-final classes, since you fundamentally can't retroactively add required initializers to classes to make them conform to new protocols:

extension NSURL: ExpressibleByStringLiteral {
  required init(stringLiteral: String) { // error: can't retroactively require this of all subclasses
    self.init(string: stringLiteral)!
  }
}

In many of these cases, the operation is not really used in practice on all subclasses; things like coding on views are often not exercised in practice on all view implementations, and convenience initializer interfaces on class clusters like NSURL or NSString are not generally expected to work on an arbitrary concrete implementation. In the spirit of Swift's other escape hatches, it'd be nice to have a way to explicitly say "I know this initializer is formally required but I'm OK with it only working on specific classes that implement it." As a strawman for want of a better syntax, we could spell this as required!:

protocol FantasyCoding {
  init(coder: FantasyCoder)
}

class FantasyView: FantasyCoding {
  required! init(coder: FantasyCoder) { }
}

class MyView: FantasyView {
  var x, y, z: Int

  // ok not to override init(coder:) here
}

extension FantasyURL: ExpressibleByStringLiteral {
  // OK to add a required! initializer in an extension
  required! init(stringLiteral: String) { .. }
}

A required! initializer would trap dynamically if invoked on a subclass that does not or cannot provide an override implementation.

Several years ago, I had proposed introducing non-inherited protocol conformances as another way of addressing some of these expressivity issues. However, that would complicate the type system, and in particular the already-complicated interactions between subclassing and protocol polymorphism, even further. Also, while that feature would address the "retroactively extending a class to conform to a protocol" use case, it wouldn't address other places where strict required creates problems, such as within purely class hierarchies, or in cases where you do want some subclasses to still be able to selectively override protocol behavior, such as when there are multiple inherited class cluster roots as with NSString and NSMutableString. On the other hand, a feature like required! would be a general escape hatch for when initializer inheritance is needed to satisfy the type system but not necessary in practice.

17 Likes

I feel like a prize idiot each time I am forced to write a trapping implementation of init(coder:) for a view controller. It would be great for this to go away if reasonably possible.

3 Likes

Thinking differently, would allowing optional initā€˜s make any sense here (I donā€˜t mean failing initā€˜s)?

protocol FantasyCoding {
  optional init(coder: FantasyCoder)
}

Someone who wants to instantiate will have to unwrap and see if the sublass supports that init or not.

We do not want to support optional requirements except as necessary for ObjC interop. This also still puts responsibility on the protocol author to anticipate the need for retroactive conformances by classes, and leaves the user no escape hatch if they fail to do so and the author no resilient way of fixing their mistake.

I'm not a fan of this pitch. If I subclass UIView I would like to be reminded by a compiler that I am supposed to provide that init.

Notice that every hole-poking tool you mentioned is used in the place that doesn't fit. ! for unwrapping optionals is used when you are sure that value won't be nil, as! is used when you are sure cast won't fail, and try! is used when you are sure that the call won't throw. On the other hand required! would be used when someone, somewhere else may or may not write a subclass which may or may not support that init.

I would rather write the crashing line in the place that doesn't fit the type system, which is my broken subclass. And I think that typing fatalError is a good way to do that.

9 Likes

That's a reasonable stance for some cases. It's impossible in the extension case to write the trap on the "place that doesn't fit", though, since the extension is what creates the misfit. The module containing the extension may not be able to see all subclasses.

4 Likes

I think that this might negate the goal of this but I would be ok if there was simply a shorter way to indicate that the required init should trap. So, everything you have but

class MyView: FantasyView {
  var x, y, z: Int

  !init(coder:) // terrible syntax but it's just an example.
}

edit: the benefit being that it is shorter but still explicit in all places what is happening with regard to the initializer.

3 Likes

This came up before in the context of Decodable: Why you can't make someone else's class Decodable: a long-winded explanation of 'required' initializers. Specifically, I had these two solutions in mind:

Future Direction: 'required' + 'final'

One language feature we could add to make this work is a required initializer that is also final. Because it's final, it wouldn't have to go into the dynamic dispatch table. But because it's final, we have to make sure its implementation works on all subclasses. For that to work, it would only be allowed to call other required initializersā€¦which means you're still stuck if the original author didn't mark anything required. Still, it's a safe, reasonable, and contained extension to our initializer model.

Future Direction: runtime-checked convenience initializers

In most cases you don't care about hypothetical subclasses or invoking init(from:) on some dynamic Point type. If there was a way to mark init(from:) as something that was always available on subclasses, but dynamically checked to see if it was okay, we'd be good. That could take one of two forms:

  • If 'self' is not Point itself, trap.
  • If 'self' did not inherit or override all of Point's designated initializers, trap.

The former is pretty easy to implement but not very extensible. The latter seems more expensive: it's information we already check in the compiler, but we don't put it into the runtime metadata for a class, and checking it at run time requires walking up the class hierarchy until we get to the class we want. This is all predicated on the idea that this is rare, though.

That second one is pretty close to what you're describing.

However, that post ends with

The Dangers of Retroactive Modeling

Even if we magically make this all work, however, there's still one last problem: what if two frameworks do this? Point can't conform to Decodable in two different ways, but neither can it just pick one. (Maybe one of the encoded formats uses "dx" and "dy" for the key names, or maybe it's encoded with polar coordinates.)

So I'm not convinced yet that this is a good idea. The two examples you've shown are an imported Objective-C class, which can't take advantage of this unless we change the compiler, and conforming a type you don't own to a protocol you don't own, which is *cough* something I'm supposed to be leading discussions on, Retroactive Conformances vs. Swift-in-the-OS. Neither of them are sufficient to motivate this feature in my opinion.

7 Likes

I picked examples that come up commonly in the field, but I don't think it's unreasonable to also want to extend a framework class to conform to a protocol you control. (If you look at the previous thread I linked, there are further examples from users trying to do exactly that.) The questions around retroactive conformance to protocols you don't own are separable, and the fact that Cocoa has run into these issues seems to me like a good indicator that other developers' libraries would as well given enough time, even if it's too late for us to do anything about Cocoa itself. (Making a required initializer like UIView.init(coder:) into a required!-with-trap one would be a non-source-breaking change, so it isn't out of the question that we could modify the compiler to take advantage of something like this given enough time.)

Speaking specifically on the NSCoding problemā€¦

Aside on retroactive conformance

(Iā€™d definitely like to add retroactive conformance to Foundation types, and not have the literal conformances defined in the overlays to prevent me from subclassing.)

I can agree that the current state of things can be annoying if youā€™re making a view/view controller you donā€™t intend to use in a nib. Iā€™m not sure that inherently means it should be addressed. For instance, unlike SE-0217, the fix-it is perfectly adequate and if you manage to hit it at runtime the problem is extremely clear.

Iā€™m way more concerned about someone accidentally starting to use such a type in a nib without knowing it wasnā€™t meant to be, and the inevitable mysterious crash at runtime that entails. To me, trapping is not an acceptable trade-off if the author did not type their way into acknowledging a trap could happen.

I understand this is cutting the Gordian knot, but I absolutely do not want a safety regression because a legitimate error was called annoying.

2 Likes

Would it be possible (within the realms of source compatibility) to move init(coder:) to being a failable initializer?

If so, it would be sound to inherit a default initiializer that returns nil. We'd need some way to spell that new concept, but it would be a lot nicer to add something like this than to add a special case hack that is known to be unsound.

-Chris

3 Likes

There's nothing unsound about trapping; this would be making the invariants around required initializers dynamically rather than statically enforced, like our other ! operations. I agree it'd be nice to have a variant of this functionality that defaults to nil or raising an error when a subclass doesn't provide an implementation, though.

Well I imagine an optional keyword to be purely a syntactic sugar over default implementation synthetization. At least it would work nicely with functions when we get compound names.

For example:

protocol P {
  optional init(coder: FantasyCoder)
  optional func doWork(on object: Object)
}

// Then the compile generates behind the scene the following extension
extension P {
  static var init(coder:): ((FantasyCoder) -> Self)? {
    return nil
  }

  var doWork(on:): ((Object) -> Void)? {
    return nil
  }
}

I donā€˜t understand the resistence regarding the keyword since with compound names this whole thing becomes pure sugar without much compiler magic, or at least I vision it like that.

The result might need to be non-optional when the function can be reached, but itā€˜s completely different if the function always returns an optional result. Sure the result overlaps but the semantics of an optional function with non-optional result are different from a non-optional function with optional result, again at least from my perspective.

1 Like

init?(coder:) is already a failable initializer, so this would technically be possible.

However, I want to echo @jrose's sentiment that I'm not sure how good of an idea this is. Specifically:

  1. I'd prefer that we not over-promote retroactive modeling of types you don't own ā€” this is something I feel we're already a bit too permissive with, though that won't change. Folks ask about extending others' types with Codable a lot, and Jordan's "The Dangers of Retroactive Modeling" section covers my concern aptly. The right answer is not to add your own conformance to types you don't own, but to write a stable adaptor type, especially where things like data integrity over time come into play
  2. Making this behavior completely implicit for subclasses feels wrong. Inheriting from a type which declares this can easily leave you in a situation where you might not be aware you're inheriting an initializer which traps ā€” especially in the case of {UI,NS}View and the like, it's trivial to end up in a situation in which you've inherited an init?(coder:) you're not aware of and placing your view in a nib, crashing in in the code you didn't write for opaque reasons. (Folks already don't always know that init?(coder:) is what's called when initializing from a nib; it's already a confusing entry point)

At the very least, I like @griotspeak's suggestion that you still have to include the initalizer explicitly, but we make it easier to express that the implementation traps rather than have to repeat the same fatalError everywhere.

8 Likes

It is probably too late to add this, but it would be interesting to have a way to explicitly express in which ways a variable's type can vary. Being able to say: "This is exactly this type" as opposed to "This is this type or a subtype" would be very useful in some cases.

In the example of these initializers, you could have a type of method/init which is only available from:

  1. That exact type (not subtypes)
  2. when called from super on subtypes

Trying to call it on a variable which could also be a subtype forces you to deal with the case that it isn't available (most likely with ? or !)

It would also be interesting to be able to specify other variances which aren't standard in OOP. For example, this variable is either this type, or a function/closure which evaluates to that type. Another one I run into fairly often is: This is either this type or a collection of that type.