Zhouwei Zhang wrote an article recently called “These 299 macOS apps are so buggy, Apple had to fix them in AppKit.” Ignore the clickbait title for now; the article shows that macOS’s AppKit framework reverts to older, sometimes buggier behaviors when it’s running certain high-profile applications. Microsoft has long had a reputation for bending over backwards in their OS to keep apps running well; Apple has not had such a reputation but it seems like they’ve still done plenty of that kind of work.
This reminds me of another article I read recently, “What’s better than semver?” by Graham Lee. In discussing the Semantic Versioning style of version numbering, where version numbers look like x.y.z and a change to z indicates “backwards compatible bug fixes,” he writes,
There is no such thing as an “internal change that fixes incorrect behavior” that is “backwards compatible”. If a library has a function
f()in its public API, I could be relying on any observable behaviour of
f()[…] If they “fix” “incorrect” behaviour, the library maintainers may have broken the package for me.
As longtime Apple employee Chris Espinosa said on Twitter of the AppKit special-casing, “Often [third-party developers] were working around our bugs.” That’s why the “so buggy” part of Zhang’s headline is undeserved. It’s also why it’s usually silly for a library developer to claim that they’ve fixed a bug but remained backward compatible. Sure—if your library contains an algorithm that could be O(n) but is accidentally quadratic, fixing the performance while keeping the semantics the same is backward-compatible. Adding a new API is usually backward-compatible. But fixing a bug in your sorting algorithm—as Apple apparently did one time, breaking some Microsoft Office apps—is potentially a breaking change, and I think this is something that library authors (and versioning-scheme authors) need to consider.1
Contrary to Clojure’s Nihilistic Versioning philosophy (“version numbering is hard so let’s not even try”), I think that making breaking changes is okay. Bugs happen, but bugs should be fixed even if that breaks someone’s workflow. Rewriting a library to take advantage of the lessons you’ve learned makes things better for you and for your future consumers.
The most important thing for library authors is just to communicate what’s changing in each version. This should happen at two levels: the eye-catching but unspecific change in a version number and the more detailed and nuanced2 information in the project’s changelog (or GitHub releases page). It’s great if you follow a well-defined scheme for assigning version numbers but writing good documentation might be even more important.
Haskell’s Package Versioning Policy uses numbers like a.b.c, where both a and b are part of the “major version number.” This lets you distinguish between smaller breaking changes, like fixing a sorting algorithm hopefully is, and larger changes like rethinking a library’s interface. ↩︎
It’s possible for a library to have a “breaking change” that turns out not to actually affect any of its consumers—in fact, this is more and more likely the more pedantic you get about what constitutes a breaking change. Along the same lines, version numbers matter a lot less if you know every single one of your consumers and can discuss potentially-breaking changes with them beforehand. ↩︎