[2016] Breaking changes considered essential

Version numbers are hard. We as programmers are awful about tracking what constitutes a breaking change compared to what constitutes an invisible bugfix. From a formal semantics perspective, there is no such thing as a bug and every “bugfix” is strictly speaking a breaking change. And yet this notion of “fixing” behavior “broken” by shoddy implementations is central to modern notions of libraries, dependency management and deployment. I’ve previously written about versioning, its semantics and some theories about how we could do it better.

This time I’m flogging a different horse: the unavoidable necessity of change in software.

Bugs are the obvious motivator for change in software artifacts, especially in this open source world. Users or “the community” finds bugs, maintainers find bugs, some combination of these conspire to fix bugs found and in theory release those fixes back into the wild. We as users all want to take advantage of these changes. SemVer, Elm’s versioning, repeatable builds and much other well established practice helps to ensure that taking advantage of “incremental” changes and fixups is tractable.

But bugfixes aren’t the only changes we may want to encourage.

Why do we want libraries which persist unchanged forever? I once had a conversation with a senior engineer in which he held forth as a good thing that here today in 2019 we’re running FORTRAN code unmodified since the mid 80s via whatever $NEWLANG to libc to libfortran monstrosity we need to cook up to do so.

There is an interesting argument to be made, implied by Steele’s ‘98 OOPSLA keynote, that a language is not simply a compiler, interpreter and other runtime infrastructure. Rather, it is somehow the semantics defined by the runtime, the semantics and critically the style of the standard libraries and those of libraries which users choose to write and which gain adoption.

This may sound obvious, but think about it for a minute. The semantics of the language and its standard library clearly dictate how code can be written in some language. The less obvious part is that the style of the standard library and the culture around the language dictate how code is written. Consider Ruby and Python. The language cores are immensely similar. They’re both object oriented VMs with modular imports and object oriented standard libraries. In both languages object introspection and metaprogramming are possible. Yet techniques like metaclass hacking dominate in Ruby when they are pretty unusual in Python. Designs which would be considered idiomatic in Ruby would be strange in Python and vice versa.

Worse, as languages and communities age, change occurs. The performance characteristics of the language may change. Libraries are invented. Features may be added. Blog posts on style written. Talks and experience reports on patterns given. Programs which once were too slow to write may be be fast enough now to be commonly used. In short, better patterns of program design appear as the domain to which a language is applied becomes well explored. Designs once considered excellent will age badly or ossify, their limitations recognized at least by some. However in the process of this learning, the community will write a bunch of code whose limitations won’t initially be appreciated.

Considering the microcosm of Clojure, early Clojure libraries stand out clearly from more recent work. They are essentially thin layers around Java APIs which perhaps didn’t need wrapping. They may not compose well, they may make pervasive use of mutable state, they may not offer good facilities for debugging, introspection, static understanding and so forth. More recent work is characterized by using reified effects, state values, reliance on functions rather than on macros, avoidance of kwargs and other design choices too numerous to list.

So if we take this conception of a language as the semantics of its standard library and of its libraries, we can clearly say that the semantics of Clojure have changed over the years as everyone has gotten better at writing Clojure and as the Clojure community absorbed more cross pollination from the pure (and purer) functional languages. Can we then say that Clojure code circa 2007 is the same Clojure that we’re writing today?

Arguably it is, as of Clojure 1.3 when the clojure.contrib namespace was deprecated and removed, not much has changed. Many things have been added such as the arrow macros, transducers, EDN and still more, but all the old code probably works.

And we shouldn’t be using it!

The changes in style that have occurred are I argue more than simple changes in fashion. Perhaps one could argue that if naming style changed (which perhaps it has) migrating from one library to another which has more stars or a nicer webpage or better names is a waste of time. The problem with this view is that it minimizes the many cases when APIs that seemed like a good idea at the time turn out to be badly designed in the long run and should be replaced.

Clearly there is great value in the Java approach of maintaining compatibility for all time. This means that the library base can grow to be enormous and most tasks become exercises in evaluating and plumbing together libraries.

However as many of the older Java APIs are evidence, it is not the case that one API no matter how stable or well defined initially remains appropriate for all time. The Java collections API is simply too big and assumes too much mutability to apply to new collections such as Clojure’s which are immutable. APIs which use Enumerable instead of its successor Iterable are one example of this. Another is the decision for various Java core classes to be final such as Pattern, precluding other tools which may target the JVM from offering compatible functionality.

The critical aspect of these design choices is that they all seemed reasonable at the time and only later as the language and community evolved were they thought better of. Call this conceptual debt if you will. For all the advantages of compatibility, I claim that it is a structural misvaluation of engineer time going forwards to claim that the price of upgrading across changed APIs is always lost.

In the presence of pervasive immutability and purity, APIs can be safely abandoned in place. They may be inadequate or map poorly to potential use cases, but their continued presence has no carrying cost. This isn’t true for mutative APIs - the semantics with which we manage physical memory, process protections and other resource management(s) cannot be so abandoned. There the continued presence of the old APIs and their mismatches the desired semantics continually undermine the desired semantics.

Two solutions come to my mind.

The first is to undertake the mighty project of ensuring compatibility cannot be breached at any point in the future. Unison is a fascinating attempt to design a language which provides precisely this property - all code remains valid for all time. While I applaud the effort, it’s not clear to me that this is possible or even desirable. Software seems to be the business of inventing words, attempting to presage all their contexts, meanings and uses. Choosing ground rules for a programming system which enforce infinite forwards-compatibility creates rather odd cases of undesired consistency and makes the escape hatch perhaps a bigger hammer than we wished. In short, semantic breakage remains possible if not needed and it’s not clear that being able to travel back in time to the old ‘stable’ code is a valuable property given those limitations.

The second and perhaps more obvious one is to relax the constraint of unending support for all previously valid programs, and admit that we’re in the business of planning and managing change. The breaking changes between Python 2 and 3 is a perfect example of such a thing. In order to deal with structural issues in the language, the decision was made to undertake breaking changes. I think that many see this as a bad thing because it served to fragment the community and because Python 3 was not adopted as rapidly as anyone involved would have wished it to be. But it’s an interesting case study in such a thing and the effects which it has on the community ecosystem.

This is not to say that I think breaking changes should be undertaken lightly or frequently. Unless undertaken carefully and with due notice, breaking changes only serve to tire out users and library maintainers. If only for his conception of users finite willingness/ability to learn, I think that Brian Goetz’s talk at Clojure/Conj 2014 was worthwhile.

Brian Gotez and I are fundamentally at odds here, as he admits that he’s at odds with Rich. Brian quotes Nikos Kazantzakis:

You have your brush, you have your colors, you paint the paradise, then in you go.

I think the folly in Brian’s argument is in the concept that, at least for software, there is another approach. From a formal specification standpoint, most changes even “patches” and “bugfixes” are breaking on a formal semantics level. As DeWayne Perry is fond of saying

We are in the position of minor gods, able to build rocks which we ourselves cannot move again

Now there is an argument to be made that some software is truly “finished” and need never be changed. Old numerics code in FORTRAN is the usual case study of this. Untold metric grad student souls have been poured into ensuring the correctness and performance of this software. Breaking let alone even replacing these programs is a simple waste of effort.

To this argument I have no explicit counter. I have a utilitarian argument in that there are exceedingly few such libraries, as they cover only well understood domains. Newtonian physics for instance is well understood and there is little need for improvement. Likewise numerics libraries. Has the definition of matrix multiplication changed? However the overwhelming majority of the tools which we use are neither of such vintage nor of such quality. Database drivers come and go. HTTP clients are a dime a dozen. If cleaning the slate of scratch work comes at the price of repeating foundational formulas occasionally that is the price of progress.

In my research for this article, I came across a quote on StackOverflow (source)

For every evangelical programmer/blogger there are 1000 avid blog readers that immediately re-invent themselves and adopt the latest techniques. For every one of those there are 10,000 programmers out there with their nose to the grind stone getting a days work done and getting product out the door. Those guys are using tried and trusted techniques that have worked for them for years. They wait until new techniques are widely adopted and show actual benefits before taking them up. Don’t call them stupid, and they’re anything but lazy, call them “busy” instead.

This is more I think the unseen enemy. The argument Brian makes is that it takes “a certain kind of hubris to say that the code one wrote shouldn’t be written that way anymore”.

The central tenant of tool, library and language development is that we do not have tools which are appropriate to our present needs, regardless of how appropriate they may have been to our previous needs. If we had said that writing assembly was good enough and that’s the way all programmers should program for all time because it’s the way that programmers already knew how to program then why do we have the incredible diversity of tools available today? We’d all be better served banging registers together by hand like cro-magons with rocks.

If this is not a patent argument for stagnation, I don’t know what is. The thesis of this argument is that the incremental costs of teaching programmers (or rather of programmers learning whether personally or corporately) to use new tools, new libraries, new styles does not justify the returns in productivity and defect rate.

So where does this leave us.

So what to do? Light it all on fire. Eventually. Tastefully. When it’s clear that it must burn.

^d

Thanks to Angus ‘goose’ Fletcher for reading a draft of this essay.