[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.)

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

Reply via email to