61 comments

[ 5.1 ms ] story [ 78.0 ms ] thread
IBM, on zSeries mainframes and z/OS, has even bigger commitment to backward compatibility than Microsoft.

Yes, it might take a long time to fix some mistakes, but usually, you get another better API added that doesn't have the old flaws. Over time, system accrues a byzantine set of features.

Unfortunately, end result of these policies is that nobody wants to develop for those systems anymore. So the (dharmic?) wheel is reinvented elsewhere.

> Unfortunately, end result of these policies is that nobody wants to develop for those systems anymore.

Is that the main reason? I always assumed "I can't buy these machines for less than a year's salary" was the major difficulty onboarding.

That's an interesting question. In the late 90s, IBM had a competition in the same S/390 architecture, with Fujitsu and Hitachi mainframes. These could compete with IBM on price, so I don't think this would be a problem. It's hard to discern cause and effect here - maybe mainframes are expensive because IBM held to them and eventually became a monopoly.

But I think the claims "mainframes are legacy and complicated" came sooner than that, and that was the real cause why people wanted to avoid them.

Compare this with Unix in the early 90s, it had a very same problem, being somewhat expensive and inaccessible, even for home PCs. But this changed with Linux... and in general, home PCs changed the expectation of the onboarding, before that, the fact that you cannot buy a computer at home, didn't present a significant obstacle in employee training.

I am almost wondering if any complex enough system is long-term commercially viable. Closed-sourcing somewhat inevitably means bleeding mindshare.

> if any complex enough system is long-term commercially viable

I don't think you can determine this from first principles without taking the wider world into account. Things like the electricity grid or car manufacturing seem to have managed just fine over long periods of time, despite (almost) nobody having their own coal power plant or cylinder boring machinery. There are some niches in software that will survive just fine even as closed source software, SAP, Salesforce and industrial control systems spring to mind.

>electricity grid or car manufacturing

electricity grid became a commodity (I believe arguably car manufacturing did as well), not sure if the theory isn't that any system that lasts over the long term will either become commoditised or cease to be commercially viable.

Electricity feels like a commodity (although unlike the other commodity markets there's almost no "carry trade", nobody talks about contango power). The electric grid is a utility; on of those things that's either a state monopoly or a state-shielded private-ish company, because there can't be competing electric grids.

Grid equipment itself is manufactured by unglamourous heavy engineering firms like Siemens or ABB.

> That's an interesting question. In the late 90s, IBM had a competition in the same S/390 architecture, with Fujitsu and Hitachi mainframes. These could compete with IBM on price, so I don't think this would be a problem. It's hard to discern cause and effect here - maybe mainframes are expensive because IBM held to them and eventually became a monopoly.

I'm sure those were still really expensive though.

IIRC, IBM made a big investment to migrate to a 64-bit architecture, but none of its competitors in the mainframe space wanted to do the same.

> Is that the main reason?

I think it is mostly the effect of them being legacy systems. Old systems become legacy systems if they are developed further. They can be difficult to work with but the mere fact that the language or platform breaks compatibility does not make that platform any better.

True, and maybe it's the same effect that drives ageism. Young things show promise and potential, while the old things are whatever they are, unsurprising, set in their ways.
Idealism vs money and staving off legal issues. Heh.
Yes.

That's the point.

No it's not a real problem.

Want to fix your mistakes? Make a fork and try harder next time.

It's such a presumption to assume that I, as the user, gives a crap about what you think are your mistakes or not, or how important they are to you, or how much cleaner the code will be when you fix it.

If you create work for me.

I will walk.

lol, indeed. That's the point of engineering, to fence and mitigate errors - both current and future, isn't it?

The future part especially makes it a hard problem.

I'd say nonsense.

Backwards compatibility is better than any other option. From time to time you need to provide new, clean versions of old APIs and that's it. Things get deprecated (as in "best not to use this unless you are aware of the consequences". Aggregate over time, don't replace. Garbage collect when not reachable. This works for APIs as weel as for functional programming data structures.

Clojure(script) approach is a good example of how this can work beautifully well in practice. Java also used to be reasonably good in this regard. Javascript (the language) is good. Javascript (the ecosystem) is criminally bad.

Suppose you create an API that allows users to enter nonsensical data or do things that are very bad for performance. Later, you realize your mistake, create a new, improved version of your API, and deprecate the old API. However, as long as you keep the old API for backward compatibility, you will have to deal with nonsensical data and poor performance.
And not keeping the old API around incurs work, potentially a lot, for everyone using the thing - transitively.

We've seen this with Python2->3 transition. If the new version isn't backwards compatible (requires non-trivial work to transition), what you've done isn't an upgrade: you've created a new product, and deprecated the old one. As we've seen from Google, people hate it when that happens (how many different eras of deprecated chat app are Google on now?)

Other examples: Windows has, on a couple of occasions, attempted architecture transitions. Windows RT was the latest. They've not yet made the leap to ARM, despite having cutdown versions of Windows (CE) running on ARM for something like 20 years. Apple have managed multiple architecture transitions by providing emulators, bridges, "fat binaries" that work across the transition, and a history of being willing to tell both developers and customers to get stuffed if they don't like it.

Windows ARM has an emulation layer for x86 binaries
> And not keeping the old API around incurs work, potentially a lot, for everyone using the thing - transitively.

Not only that, but breaking backwards compatibility will cause dependency conflicts once a dependency graph is a bit larger, and this can easily completely stall transitioning of software to a new version.

You won't have to deal with it. Whomever is using that old API has to deal with it. Their choice.

We are talking about an API, not a shared service.

We are talking about an API, not a shared service.

That distinction is becoming a lot blurrier than it used to be as more and more "APIs" now are run as shared services with no option of running them locally.

To be clear: my personal opinion is that Linus' attitude about not breaking user space is the absolutely right one.
The possibility of entering nonsensical data does not break old software.
It may be possible to create a programming language where you express your intent at enough of a high level that the code can be automatically refactored into using new APIs.
The reality is that you have to be sensible and balanced.

You can't keep BROKEN functions "because people rely on them", but you can't suddenly make a minor release (e.g.: from 2.3.4 to 2.3.5) completely changing the semantic of a feature you expose.

You need to:

- Avoid changing APIs just for the sake of change.

- Announce breaking changes.

- Document how users are intended to migrate from your old API to the new one.

Very large projects like Django do things like:

- v2.0: Adds a new API, but keeps the old one

- v3.0: Removes the old API.

This gives consumers of the API plenty of time and margin to experiment with both and move on.

If a single release changes too many things, it's very hard for a developer to upgrade, since they have a completely broken codebase, and need to fix multiple breakage at once.

> This gives consumers of the API plenty of time and margin to experiment with both and move on.

It's not a silver bullet though, as we saw with the move to Python3. It annoyed people with large Python2 codebases, giving them the choice between:

• Laboriously reworking their code to transition it to Python3 (a language with only limited advantages over Python2)

• Using an unsupported language for a significant codebase

Python 2-3 is a great COUNTER example of what I'm saying.

There was no intermediate release that could run both python2 and python3 code.

It introduced new features AND removed the old ones all at once.

> There was no intermediate release that could run both python2 and python3 code.

Right, but would that have helped? Transitioning a large codebase from Python2 to Python3 is a significant undertaking, there's no way around it.

As I understand it there are dedicated tools to help migrate Python code, but it's still no small thing if you're dealing with a large codebase. Perhaps it would have been helpful to have an interpreter that supported the union of both languages, but I don't see that this would have changed things from significantly costly down to no big deal. It would make the process more incremental, but it would still represent significant work. There are legitimate reasons people don't want to have to modify large stable codebases.

(To be clear, I haven't had to do this personally, but that seems to be the sentiment of people who have.)

C++, on the other hand, goes to considerable lengths to preserve backward compatibility, at the cost of accumulating complexity.

It would have helped immensely.

The main issues with the upgrade is that:

1. Libraries needed to maintain both versions (or code compatible with both.

2. Projects couldn't upgrade until their dependencies did.

If Python 3 could run old code (for example you needed to write `import py3` at the top of a file to switch) then all projects could have updated their code in parallel, there would be no (or minimal) work to maintain support for old versions and everyone can update to py3 the day it came out. The workflow then looks like this:

1. Everyone updates to Py2.9 which supports the old features and the new features.

2. Everyone migrates their code to use the new features and remove deprecated features independently.

3. Py3 comes out and drops support for the old stuff.

1 is not trivial, but it is equivalent to any other time that you drop support for old versions, this happens regularly and isn't a huge event.

Compare that to the nasty wave of updating dependencies and requirement to continue supporting old versions, especially since the code to support multiple versions was fairly awkward for a lot of projects.

It helped for Ruby. Ruby released 1.8.6 and 1.9.1 which were both two intermediate steps between Ruby 1.8 and 2.0. 1.8.6 beibg similar to 1.8.4 but with many added 2.0 features and 1.9.1 which was almost 2.0 but with some deprecated features in it.

I got to experience both and the Ruby 2 migration was much smoother than the Python 3 one.

EDIT: To be clear, Ruby did not make it literally possible to run both 1.8 and 2.0 in the same code base but with just a few conditional statements and avoiding to use non-backported 2.0 features it was easy to write libraries which could run in both 1.8.6 and 1.9.

> Right, but would that have helped? Transitioning a large codebase from Python2 to Python3 is a significant undertaking, there's no way around it.

I haven't migrated from python2->3 however I've worked on projects in both. I think a "python2+3" version would have helped. With both APIs in place new code can be written the new way and old code can be migrated as it is touched. Having the ability to incrementally change code I think encourages the transition since you won't end up with a pile of broken code.

You could transform your 2.x code so that it would run on both 3.x python and also on recent 2.x python versions, using new features with appropriate `from __future__ import` statements; so in that regard the latest 2.x versions were those intermediate releases that could also run 'new style' code. I had a bunch of internal code that would run on both 2.x and 3.x python, and many libraries also did just that.
So why does the Python Wiki runs Python 2 with MoinMoin Wiki? You seem to imply it is easy and quick to rewrite software to match it to new versions, and resources for this are always available?
Depends on what you mean with "BROKEN". Breaking changes actually break other people's code, tutorials, documentation, tooling and developer knowledge often for very questionable gains.

There are many projects that are relied on by thousands, but they keep pushing breaking changes on a regular basis without providing tangible improvements. This is especially true for tooling, frameworks etc. for web development.

Hypothesis: this is especially true for corporate-sponsored open source where the devs also sell consulting services - i.e. "freemium" - because their incentive structure does not encourage making things easy to use without their services.
Most of what you touch on is covered by semantic versioning. (https://semver.org), and IMO should be mandatory reading by anyone in software development.

It sort of looks like you are sort of aware of it, but maybe not entirely. What you called a minor version is typically called a "patch" version. So it goes. MAJOR.MINOR.PATCH

Major version bump can change anything. Minor versions are generally safe to upgrade, but will change the api in a backwards compatible way. Patch version updates make no change to the api.

One of the worst things to deal with is libraries for which maintainers have broken the semver contract.

Qt is exemplary in that. There are ironclad rules that only MINOR adds new features and only MAJOR breaks API. And PATCH is strictly for bugfixes.

Binary compatibility is guaranteed as well so that an application compiled against 5.0.0 will dynamically link against 5.15.2 and run fine unless it relied on clear bugs.

There are exceptions for things like input method plugins, for which one needs to explicitly opt in to using internal API with build sysem flags. Without the opt-in, the corresponding private headers are not accessible.

You have to define the notion of "backwards compatible" though, as this is the core of semver. To be strict, a bugfix is not backwards compatible. You explicitly change the behaviour of a function or interface.

There's an interesting paper on checked semantic versioning [1] and you will get a new MAJOR version if you don't explicitly add the NOCOMPARE-pragma to a bugfix, as the prover (obviously) can not show the original function and the bugfixed function to be equal (usually they are not for some input).

Backwards compatibility is hugely in the eye of the beholder (Hyrum's Law). So this is not a cut-and-dry issue.

[1] https://drops.dagstuhl.de/opus/volltexte/2018/8456/

I think the underlying assumption here is that this is versioning for humans (and package managers). It isn't 100% ironclad and fool proof. It really only tries to make the job better/easier for those who rely on other libraries by baking in an educated answer to the following question:

> If I upgrade the library from X.Y.Z to A.B.C, what can I expect to have changed, and to what extent do I need to do anything after I make the upgrade to make my code work the same?

I don't find it interesting that, technically, any observed change to the library can potentially break code. If I use reflection to branch on number of methods available, a bump in minor version can introduce a method to the API, and thus break my code. Making an observation of this is one thing, naming it after one self is a tad on the egotistical side.

Other than that, I agree completely that the difficult part is defining what "backwards compatible" really means. However, to be clear, changing behavior is not the same as being backwards incompatible. I expect bugfixes to make my code function more correct and more reliably. I can reasonably expect minor versions to add functions or features that don't get in the way, and I can expect nothing from major version upgrades, so those will always need to be done with some extra care.

The biggest problem I have with semver is that we don't have good enough tooling to reject minor or patch version bumps when major version bumps are actually warranted. I'm not sure it's possible to have that thorough of analysis in general.

Semver is handy for understanding what the maintainer meant to do. I guess in a lot of circumstances that's not nothing.

> The biggest problem I have with semver is that we don't have good enough tooling to reject minor or patch version bumps when major version bumps are actually warranted. I'm not sure it's possible to have that thorough of analysis in general.

The biggest problem with automated tooling is that there is a nontrivial subjective component to this. If you look at things strictly, any bug fix that changes externally observable behavior can be a breaking change - but bumping the major version for every release defeats the purpose of SemVer.

If the change actually breaks existing use, then bumping the major version of every such release is *exactly* the purpose of SemVer.

One important note that is often overlooked, as it isn't part of the short SemVer summary, is that major version 0 is special. The rules for minor and patch versions do not apply, and anything can change at any time.

In other words, a project and library can explore its API freely and make as many wild changes as it pleases. Once it matures, it can be made 1.0.0, and the whole point of doing so is to communicate that the rules of major, minor and patch version bumps can be expected to be followed.

I think GPs main point is not covered by semver at all - namely to not break anything out of the blue but have a transition period where API consumers can upgrade at their own leisure.
I do not know what GP is short for, apologies if I'm missing your point in my reply.

One, if not the most important part of semver is communicating the nature of the changes, and in particular, how that change affects the use.

I once used a library/tool that would detect vulnerabilities. The maintainers thought it would be a good idea to turn it in to a paid service, and a minor version upgrade caused it to fail silently due to lack of a token. When I noticed it by coincidence, I raised the issue of it not being communicated properly, and they said they had made every effort at raising awareness about the change through blog posts, twitter, etc. However, few people really subscribe or follow this kind of feeds. The change that came as an increment in minor version had IMO broken semantic versioning. If it instead had been followed, it would have communicated exactly that some care when upgrading was necessary, and allowed the API consumers to upgrade at their own leisure.

> One, if not the most important part of semver is communicating the nature of the changes, and in particular, how that change affects the use.

Yes, SemVer communicates breaking changes, but only after they have happened. The point is that you should have a migration path ready long before the old API is no longer available.

What do you mean by "after they have happened"?

The changes to an updated library don't affect you until you have upgraded to that version. The whole point of semantic versioning is to allow you or package managers you use to limit which libraries to update. So, semver is to let you know of braking changes exactly before they would happen, because until you actually upgrade the library, the change hasn't happened. Surely this makes sense? Maybe we are talking past each other.

https://medium.com/@xzyfer/why-node-sass-broke-your-code-and...

What do you think of this example, where a library made a breaking, non-backwards compatible change, causing user code to throw fatal errors.

They defended their point that this is only a minor version increment since the working user code was actually erroneously working and should have been throwing fatal errors the whole time.

If it broke the exposed API, it should not be a minor.

It doesn't matter if it's "by design" or not, people still rely on it and semver is how you communicate the breakage to them.

What if it counts as a bug fix?
It really depends on the bug? If it is a bug that causes your program to crash or return obviously invalid output then it's probably OK to fix it. But if the 'bug' does something useful that just happens to be undocumented or contradicts the documentation, then you need to be a lot more careful about 'fixing' it. Especially if it's clear that lots of people are relying on that bug as a feature.
I ran into this recently, I'm sure many others did too (I'm not the issue poster).

https://github.com/eventlet/eventlet/issues/702

https://github.com/benoitc/gunicorn/pull/2581

While the bug does seem pretty critical for websocket users, the patch-version-change ended up breaking any application that uses gunicorn with eventlet (even if you don't use websockets).

It feels slightly like my fault, because eventlet has been at v0.x.x for 9 years now. I used to be a bit hesitant about using anything that couldn't declare a version 1.0.0. I guess I've learned my lesson and just won't use any v0 library except in hobby projects now.

Per hyrums law, this would mean that essentially all changes should result in a major version bump, which defeats the purpose.
It also makes "Hyrum's Law" pretty useless, doesn't it? Who names something like that after themselves? Well, I know at least one such case.
> It also makes "Hyrum's Law" pretty useless, doesn't it?

Not really. Hyrum's law is descriptive of reality and borne out of experience trying to update/change things. The point of it is to remind you that even when you think you aren't breaking things, you probably are breaking someone's workflow, and that your API surface is much larger than you think it is (performance, threading, ordering characteristics, etc. are all things that your dependencies will routinely rely on).

> Who names something like that after themselves? Well, I know at least one such case.

He didn't. Titus Winters probably deserves credit for the name. Hyrum deserves credit for the observation.

He sure makes no small attempt at marketing the term, and himself https://www.hyrumslaw.com/

In any case, my point is that it this observation has no relevance on the applicability of semantic versioning. One is a practical approach at making the best out of versioning. The other is a rather simple observation that given sufficient enough users of an API, all parts of the API will be depended on by someone.

To go and suggest that the logical extreme of a though experiment that, any change (bug fix or what have you) on an API, and given wide enough usage, will change/break something for someone, should somehow conclude in a defeatist mentality of "what's the point, everything becomes a major version", is... in my honest opinion, as uninteresting of an observation as they come. I'm sure it has more interesting applications elsewhere.

But then again, maybe I don't get what is supposed to be profound, or applicable, and I'm all ears.

> One is a practical approach at making the best out of versioning.

I think the observation is that this is sort of going to inevitably fail, and people will get mad at you anyway (perhaps more mad than if you didn't have any semver promise to begin with).

Right, like say you maintain a library and refactor something in a patch version bump. It affects something that isn't a documented aspect of your api, but is nonetheless something I depend on. I complain to you that you broke me with a patch upgrade. Well who is right? You did break me. Should I continue to trust you? Should I pin to a patch version (this makes semver useless)? Should you take more care to not break observable behaviors? How can you even know which behaviors are depended on, many will be unknown unknowns!

The ultimate conclusion from this approach is that for a widely enough used api, you will always break someone, so you need to ship a migration story instead of a change, or you need a way to assert that your change won't break any of your users, which short of being able to run all of your dependencies tests isn't possible. This can be achieved in a corporate world, but not in the OSS world. And so yes, one of the fundamental truths of software engineering is that semver is fundamentally broken. It may be the best thing we've got, but it's absolutely not a magic pixie dust. You will still break your dependencies, and your dependencies will still break you, and you should prepare for that, semver or not.

> He sure makes no small attempt at marketing the term, and himself https://www.hyrumslaw.com/

Yes, and? It seems odd to drag someone for marketing themselves, on of all places, hacker news.

I think I'm just missing what the point of the sentiment is. You brought it up in reply to the failure to adhere to the semver contract.

> Per hyrums law, this would mean that essentially all changes should result in a major version bump, which defeats the purpose.

So, what is the point you want to bring across?

> I think the observation is that this is sort of going to inevitably fail, and people will get mad at you anyway (perhaps more mad than if you didn't have any semver promise to begin with).

Yes, and? It has absolutely zero impact on the applicability of semantic versioning, its purpose, and its utility. Nor do I find this observation particularly novel, or surprising. Are you suggesting to do away with semver, because a patch update given a theoretical "enough usage", will break for someone? We already have that granularity, just lock the version you use to the exact patch, and you're good. That way, you treat any change as a potential braking change, and the end result of this defeatist attitude is covered.

> The ultimate conclusion from this approach is that for a widely enough used api, you will always break someone, so you need to ship a migration story instead of a change, or you need a way to assert that your change won't break any of your users, which short of being able to run all of your dependencies tests isn't possible. This can be achieved in a corporate world, but not in the OSS world. And so yes, one of the fundamental truths of software engineering is that semver is fundamentally broken. It may be the best thing we've got, but it's absolutely not a magic pixie dust. You will still break your dependencies, and your dependencies will still break you, and you should prepare for that, semver or not.

And again, this is just beating the same dead horse. What's the actual point in relation to semantic versioning? Semantic versioning is not "fundamentally broken", that conclusion is ridiculous. Semantic versioning an engineering best practices. These things, and "logical thought experiments" rarely agree with each other. Suggesting this is therefore fundamentally broken, with not a single clue given as to how to improve it, is what I'm referring to as a defeatist attitude. A "best effort" must improved with a "better effort". So far, I've just seen "meeh, what's the point".

> Yes, and? It seems odd to drag someone for marketing themselves, on of all places, hacker news.

I'm not dragging them for marketing themselves. I'm annoyed about the triviality of the "observation" being referred to as a named law, in a contrived attempt at credence. It doesn't help to then see this used as attempting to make a point it has no grounds for. Not every xkcd observation needs to be coined by someone and called a "law".

Versioning is always a little bit tricky. First of all, it depends what you are versioning. If it is an API (be that web-api or programmatic API through a public interface), then semantic versioning is exceedingly useful, and how to go about it is less ambiguous.

To address the issue you linked to, it really doesn't matter if it is a bug-fix or not. Semver 2.0 describes PATCH in the summary as

> PATCH version when you make backwards compatible bug fixes.

And the words backwards compatible are key. If it isn't, it doesn't matter that it fixes a bug or adds a breaking feature. If the code will break for your users unless they change their code, then you cannot fix it by bumping the PATCH version.

Now, semantic versioning isn't the end all be all. There are other versioning schemas that suit some developments. But, if there is a pretense of actually following semantic versioning, then the argument presented in defense really isn't relevant. Bumping the major version isn't something holy and beyond reproach. Just use it for what it is intended. Package systems very often rely on semver being followed. Does the new version break existing use? Then bump the major version. That's the responsible thing to do, and it should only be met with understanding.

To quote SemVer 2.0:

> 6. Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior.

Only "Announce breaking changes." is covered by semver.

A library that makes a breaking change every week could be following semver if it bumps the major version every time, but they are not taking backwards compatibility seriously.

Yeah this pushes developers of the platform between a rock and a hard place.

It's ok to keep backwards compatibility, but the API needs to also move forward and get the bugs ironed out. The worse case is when developers start to rely on the bugs for their workarounds (yes, please, do test your API before releasing)

Maybe shims are the way to go. Instead of keeping old functionality around you make a shim that only talks to the new API. And that will serve as a guide (because if you're taking functionality out that's usually not good). I've seen a couple of cases where the deprecated API was much more straight forward and easy to use than the new one. Not good.

We are talking about APIs (Application Programming Interfaces), not implementations. Of course you are free to change the implementation of your API at any moment, as long as you keep the semantic and performance characteristics.

If you have a much improved design which allows for a better implementation then it would be perfectly fine if you reimplement the old API on top of the new API. As long as this is strictly transparent (source and binary, if this makes sense), then why not.

Linus attitude about not breaking user space is the correct one.

I'm reminded of a story told by Joel Sposky about Simcity on Windows 95.

The DOS/Win3.x version of SimCity had a bug where it used memory after freeing it, which worked fine on DOS since the memory didn't get reallocated. The Windows 95 team found that it was broken, and so they added code to detect whether SimCity was running, and if so ran the memory manager in a special mode that didn't release memory immediately.

people love to credit MS's success to their ruthless monopolistic practices and there is truth to that, but they've also (a) treated developers very well and (b) put tremendous effort into backwards compatibility. Neither of these are trivial but I don't think there's another player out there who has done this as well.