Swift delight: library evolution— Jordan Rose (@UINT_MIN) November 18, 2021
In this case I don’t mean the feature for changing your library in binary-compatible ways, although that’s cool too. I just mean the general ethos of making library authors a priority.
Part of the Swift Regrets series.
In this case I’m not talking about the feature for changing your library in binary-compatible ways, although that’s cool too. I just mean the general ethos of making library authors a priority. This came from knowing, from the start, that Apple would want to use Swift to write OS frameworks. That means ABI stability and
-enable-library-evolution, but it also means making sure it’s possible to develop a library with long-term source stability. I, personally, believe a language should help you do this, and one of the basic principles to support that is “library authors should not make any commitments by accident”.
For example, in Ruby methods are public by default, and you have to say
private to change that. If you forget, though, someone might start using your should-be private method directly from outside the library. Now it’s part of your library’s interface, and fixing the accidental exposure or even changing the behavior of the method could break clients. Non-public default access control isn’t new—C++ classes use
private by default and Java actually matches Swift by using “package” visibility as its default. The idea of access control is very much linked to the object-oriented notion of “encapsulation”. In my mind, the encapsulation boundary of an object matters less than the boundary of a library.1
One place in Swift where this shows up is memberwise initializers. Swift will synthesize an initializer for a struct that doesn’t otherwise have one, but that
init is never public. It may not even be
internal, if some of the properties are private. This can be annoying if you really just want a sort of tuple-like struct, like a Point. Of course you want a public initializer to go with public
y properties. But…that’s committing to more than just the properties. It also commits to their order, as well as providing this initializer signature forever, even if the representation of the Point switches to polar, or it gains a third coordinate.2 I remain convinced this is the right behavior for synthesized initializers; as with the usual default to
internal, it’s something you only have to think about when you move from a single app module to a library or multi-module project, so the easy case is easy but library authors still get control.
Another example is non-exhaustive enums. Everyone loves exhaustive switches, but does a library author really want to promise they’ll never add another case to an enum? This is especially true for aggregate errors, things that can fail in an variety of ways. Sure, you can inconveniently get most of the effects of a non-exhaustive enum with a public struct backed by a non-public enum. But that’s inconvenient for library authors, and bad for clients too, because now the compiler can’t help them keep their switch exhaustive.3 Why would you want an exhaustive switch over a non-exhaustive enum? Because it at least means you’ve audited all the known possibilities, and when you update you’ll get a reminder to re-audit.
Thus we have
@unknown default, which warns if a switch isn’t otherwise exhaustive. You can read more in SE-0192. It’s only for
-enable-library-evolution today, but there’s been a fair bit of interest in it for plain old source packages too. Because again, library authors shouldn’t commit to things by accident, including exhaustivity.
I’m not really going to spend time on open vs. public for classes. I’ve talked about it before. But yes, sometimes making things more conservative for library authors’ benefit cuts off possible uses of an API.
We’re in a world where people do ship libraries, with versions, and make changes without breaking their users. The language should help you do that. (And also the tools should help you; Swift is definitely way behind its potential there.) Apart from Rust and maybe Java and C#, I don’t know any other languages that make library authoring a deliberate priority in this way. And I’m glad Swift does.
In theory, “a library” doesn’t have to mean “a module” in the Swift sense. But the core Swift language doesn’t have any real notion of anything above module. Still, you could reasonably pitch “package” as an encapsulation boundary the language could support. ↩︎
Lacking in the language is a concise way to say “synthesize the memberwise initializer at a higher access level anyway”. Such proposals have gotten bogged down in design, but you can check out the past discussions on the forums. ↩︎
Dave DeLong points out that it may have been beneficial not to tie “exhaustive switch” so closely to “union of distinct payloads”, which is an interesting point. An interesting design problem for the future! ↩︎