Sent from my iPad

> On Jul 10, 2016, at 10:38 PM, Jordan Rose via swift-evolution 
> <[email protected]> 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 
> (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” 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” 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

Thanks for this really excellent, detailed analysis of the rationale for making 
sealed the default 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.)

FYI - I have been planning to propose exactly this feature for protocols once 
we get past the Swift 3 additive feature freeze.

> 
> 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
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to