On Sun, Jul 10, 2016 at 10:38 PM, Jordan Rose via swift-evolution < swift-evolution@swift.org> wrote:
> [Proposal: > https://github.com/apple/swift-evolution/blob/master/proposals/0117-non-public-subclassable-by-default.md > ] > > (This is my second response to this proposal. The previous message shared > a use case where public-but-non-subclassable made things work out much > better with required initializers. This one has a bit more ideology in it.) > > As many people have said already, this proposal is quite beneficial to > library designers attempting to reason about their code, not just now but > in the future as well. The model laid out in the Library Evolution > document <http://jrose-apple.github.io/swift-library-evolution/> (often > referred to as “resilience”) supports Swift libraries that want to preserve > a stable binary and source interface. > > In the Swift 2 model (and what’s currently described in that document), a > public class must be final or non-final at the time it is published. It’s > clearly not safe to *add* ‘final' in a later version of the library, > because a client might already have a subclass; it’s also not safe to > *remove* ‘final’ because existing clients may have been compiled assuming > there are no subclasses. > > (Of course, we can remove this optimization, and make ‘final’ a semantic > contract only. I’m deliberately avoiding most discussion of performance, > but in this parenthetical I’ll note that Swift makes it possible to write > code that is *slower* than Objective-C. This is considered acceptable > because the compiler can often optimize it for a particular call site. For > those who want more information about the current implementation of some of > Swift’s features, I suggest watching the “Understanding Swift Performance > <https://developer.apple.com/videos/play/wwdc2016/416/>” talk from this > year’s WWDC.) > > With this proposal, a public class can be non-publicly-subclassable or > publicly-subclassable. Once a class is publicly-subclassable (“open”), you > can’t go back, of course. But a class that’s not initially open could > *become* open in a future release of the library. All existing clients > would already be equipped to deal with this, because there might be > subclasses *inside* the library. On the other hand, the class can *also* be > marked ‘final’, if the library author later realizes there will never be > any subclasses and that both client authors and the compiler should know > this. > > One point that’s not covered in this proposal is whether making a class > ‘open’ applies retroactively, i.e. if MagicLib 1.2 is the first version > that makes the Magician class ‘open’, can clients deploy back to MagicLib > 1.0 and expect their subclasses to work? My inclination is to say no; if > it’s possible for a non-open method to be overridden in the future, a > library author has to write their library as if it will be overridden now, > and there’s no point in making it non-open in the first place. That would > make ‘open’ a “versioned attribute > <http://jrose-apple.github.io/swift-library-evolution/#publishing-versioned-api>” > in the terminology of Library Evolution, whatever the syntax ends up being. > > --- > > Okay, so why is this important? > > It all comes down to reasoning about your program’s behavior. When you use > a class, you’re relying on the documented behavior of that class. More > concretely, the methods on the class have preconditions > (“performSegue(withIdentifier:sender:) should not be called on a view > controller that didn’t come from a storyboard”) and postconditions (“after > calling loadViewIfNeeded(), the view controller’s view will be loaded”). > When you call a method, you’re responsible for satisfying its preconditions > so it can deliver on the postconditions. > > I used UIViewController as an example, but it applies just as much to your > own methods. When you call a method in your own module—maybe written by > you, maybe by a coworker, maybe by an open source contributor—you’re > expecting some particular behavior and output given the inputs and the > current state of the program. That is, you just need to satisfy its > preconditions so it can deliver on the postconditions. If it’s a method in > your module, though, you might not have taken the trouble to formalize the > preconditions and postconditions, since you can just go look at the > implementation. Even if your expectations are violated, you’ll probably > notice, because the conflict of understanding is within your own module. > > Public overriding changes all this. While an overridable method may have > particular preconditions and postconditions, it’s possible that the > overrider will get that wrong, which means the library author can no longer > reason about the behavior of their program. If they do a poor job > documenting the preconditions and postconditions, the client and the > library will almost certainly disagree about the expected behavior of a > particular method, and the program won’t work correctly. > > "Doesn’t a library author have to figure out the preconditions and > postconditions for a method anyway when making it public?" Well, not to the > same extent. It’s perfectly acceptable for a library author to document > stronger preconditions and weaker postconditions than are strictly > necessary. (Maybe 'performSegue(withIdentifier:sender:)’ has a mode that > can work without storyboards, but UIKit isn’t promising that it will work.) > When a library author lets people override their method, though, they're > promising that the method will never be called with a weaker precondition > than documented, and that nothing within their library will expect a > stronger postcondition than documented. > > (By the way, the way to look at overriding a method is the inverse of > calling a method: *you* need to deliver on the postconditions, and you > can assume the *caller* has satisfied the preconditions. If your > understanding of those preconditions and postconditions is wrong, your > program won’t work correctly, just like when you’re calling a method.) > > This all goes double when a library author wants to release a new version > of their library with different behavior. In order to make sure existing > callers don’t break, they have to make sure all of the library’s documented > preconditions are no stronger and postconditions are no weaker for public > API. In order to make sure existing subclassers don’t break, they have to > make sure all of the library’s documented preconditions are no *weaker* and > postconditions are no *stronger* for overridable API. > > (For a very concrete example of this, say you’re calling a method with the > type '(Int?) -> Int’, and you’re passing nil. The new version of the > library can’t decide to make the parameter non-optional or the return value > optional, because that would break your code. Similarly, if you’re > *overriding* a method with the type ‘(Int) -> Int?’, and *returning* nil, > the new version of the library can’t decide to make the parameter > *optional* or the return value *non-*optional, because *that* would break > your code.) > > So, "non-publicly-subclassable" is a way to ease the burden on a library > author. They should be thinking about preconditions and postconditions in > their program *anyway,* but not having to worry about all the things a > client might do for a method that *shouldn’t* be overridden means they > can actually reason about the behavior—and thus the correctness—of their > own program, both now and for future releases. > > --- > > I agree with several people on this thread that > non-publicly-subclassable-by-default is the same idea as > internal-by-default: it means that you have to explicitly decide to support > a capability before clients can start relying on it, and you are very > unlikely to do so by accident. The default is “safe” in that a library > author can change their mind without breaking existing clients. > > I agree with John that even today, the entry points that *happen* to be > public in the types that *happen* to be public classes are unlikely to be > good entry points for fixing bugs in someone else's library. Disallowing > overriding these particular entry points when a client already can't > override internal methods, methods on structs, methods that use internal > types, or top-level functions doesn’t really seem like a loss to me. > > Library design is important. Controlling the public interface of a library > allows for better reasoning about the behavior of code, better security > (i.e. better protection of user data), and better maintainability. And > whether something can be overridden is part of that interface. > > Thanks again to Javier and John for putting this proposal together. > Jordan > > P.S. There’s also an argument to be made for public-but-not-conformable > protocols, i.e. protocols that can be used in generics and as values > outside of a module, but cannot be conformed to. This is important for many > of the same reasons as it is for classes, and we’ve gotten a few requests > for it. (While you can get a similar effect using an enum, that’s a little > less natural for code reuse via protocol extensions.) > Would public-but-not-conformable protocols by default be the next step, then, in Swift's evolution? > P.P.S. For those who will argue against “better security”, you’re correct: > this *doesn’t* prevent an attack, and I *don’t* have much expertise in > this area. However, I *have* talked to developers distributing binary > frameworks (despite our warnings that it isn’t supported) who have asked us > for various features to keep it from being *easy.* > > _______________________________________________ > swift-evolution mailing list > swift-evolution@swift.org > https://lists.swift.org/mailman/listinfo/swift-evolution > >
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution