thoughts on tech

(i don’t have a lot of them ^_^)

interfaces: versioned xor unstable

tl;dr: when designing interfaces, always either put a version on it or make it unstable. none of this is new, it’s just a bunch of common opinions i agree with.

when you expose an interface to your implementation, a library, a protocol, a command-line tool, an operating system, or whatever else, one choice can make or break its future quality: do you promise your users stability?

interfaces describe the semantics of providers and consumers

an interface is documentation: it tells you how the code that provides and consumes it behave together. interfaces are useful because they allow for abstraction and modules: you can hide information the consumer doesn’t need to know, and you can change the provider. these things usually go hand in hand.

stability means the semantics don’t change

an interface is “stable” when the semantics of an interface don’t change within some defined bounds. that might be a version number, a time frame, or something else entirely. sometimes it’s “never”. we usually talk about this in terms of backwards compatibility.

some windows users expect DOS commands that worked 40 years ago to work the same today, and largely, microsoft has enabled this. likewise, you can run programs written for ancient versions of linux… that is, assuming they call the system calls directly instead of relying on userspace libraries. but that’s a different can of worms.

stable interfaces reduce maintenance

stable interfaces are nice! they mean that, once a consumer is complete, it will not need to be maintained to keep serving the user. this is one reason they are commonly viewed as important.

semantics must sometimes change

unfortunately for those existing programs, nothing lasts forever. cryptographic algorithms get broken, hardware changes to favor different approaches, maybe the maintenance required to maintain the old interface gets too much, maybe the old interface is just broken in some way.

backwards compatibility is one of the reasons every cryptographer i know of hates PGP with a passion. using old cryptographic primitives with dubious authentication in $currentyear is not a good idea. for something less severe, big endian is practically extinct, and requiring it in a file format today seems silly, but it made sense when MIPS and PowerPC were still worth mentioning and ARM’s bi-endianness was widely used.

versions allow producers to change the interface without breaking consumers

so what if you want to be able to change the interface when you want, but don’t want to piss off everyone who depends on its semantics? you add a version, then tell consumers to update to the new version, and after some time, remove the old version. this way, the provider isn’t stuck with the problems of the old interface, and the consumer can be fixed in time.

instability allows consumers to accept the maintenance burden

if an interface is in developmented or expected to break often, it probably makes sense to declare it unstable. of course, that means that consumers must expect to be updating all the time, but the benefit is that producers can change quickly with the expectation that consumers must update.

for interfaces, non-breaking changes are not useful

semantic versioning is a standard that defines versions of the format major.minor.patch. breaking changes, the kind we have been talking about, bump the major version, non-breaking changes bump the minor version, backwards-compatible bug fixes bump the patch version. but hold on! what’s a backwards-compatible bug fix? if you ask the specification, it’s “an internal change that fixes incorrect behavior”.

what if consumers have come to rely on the incorrect behavior? after all, if the incorrect behavior wasn’t externally visible, it probably wouldn’t need to change. unfortunately, this is so commonly observed that it has a name: hyrum’s law claims that given enough consumers, they will rely on every externally visible behavior of an interface. much has been written about this, but there are two main approaches to this: pinning dependencies to specific versions or breaking consumers if they rely on these behaviors.

an example of pinning are lock files in the javascript ecosystem: package managers like npm expect users to check a so-called lock file (not to be confused with advisory file locks) into version control, and update it regularly.

breaking consumers is common elsewhere. python’s requirements.txt allows semver specifications, and encourages consumers to match major and minor versions, not patch. on linux-based OSes, programs that depend on shared libraries are usually linked against only a major version, if any.

as an aside, symbol versioning exists too, but will be ignored for the purposes of this blog post.

when considering these existing problems and solutions, it makes most sense to me to version interfaces using a single integer.

the zig compiler and std

zig’s compiler and standard library follow some kind of versioning scheme, probably pride versioning, but are also declared to be unstable. essentially, versions are not for interface guarantees, but for goals and to provide regular “snapshots”. i don’t think i have a strong opinion on this – it’s certainly convenient for consumers to update less frequently.