8 ms·
> Due to our size, we don’t need any kind of backwards compatibility, we just update everything. Then just use 0.y.z versions and be done with it? If the libr
by cube2222 5y ago
> Due to our size, we don’t need any kind of backwards compatibility, we just update everything.
Then just use 0.y.z versions and be done with it?
If the library constantly changes and everybody expects that, then that seems fitting.
I like Go's major version handling very much. If it's backwards incompatible, it's basically a new library under the same name and development team.
In my opinion making major version updates so painful also incentivizes not making backwards incompatible changes in libraries, which results in the Go ecosystem being very stable overall (something I value a lot in my day-to-day).
- bheadmaster 5y ago>If it's backwards incompatible, it's basically a new library under the same name and development team. This, this, and a hundred more times this. Incompatible is incompatible. There is no "kinda incompatible", "99% compatible" - when it comes to dependencies, they either work properly or they don't. Software should be eternal. Without being strict about semantic versioning, it is impossible to make it so.
- slimsag 5y agoHow do you define "compatible"? Semver doesn't define it. An HTTP server can remain API compatible, and still drop or break support for a major feature you care about. Surely you don't want that to ship in a patch release. Adding a new struct field, even a private one, can break API compatibility in Go if people use your struct without named fields. Do you want a new library in this case or not? Semver doesn't cover CLI compatibility, either, do you want a CLI redesign to remain v1 or become v2? Nuance matters. Stating to "just be strict about semantic versioning" doesn't help, semver is fuzzy.
- paskozdilar 5y ago> How do you define "compatible"? I define it as "provides the equivalent API".
- travisjungroth 5y agoI’ve added a method to a class. You use the same method signature but with different meaning in your code base. My release breaks your duck typing code. Was my release compatible? Is that an equivalent API?
- paskozdilar 5y agoIf a class with its methods is a part of your API, and you add another method to the class that doesn't change the behavior of other methods, that it's a backwards-compatible release. Other than that, I don't understand your question. What does "use the same method signature but with different meaning" mean? What is my "duck typing code"? Is duck typing part of your API?
- anonymoushn 5y agoIn golang, you can reasonably have a collection of things accessed through interfaces, check whether each one conforms to additional interfaces, and call the methods of those additional interfaces if so. If user code does this, then adding a new method to a library can cause user code to break. You may say "Well, don't do that," but it seems like golang users think of this as a way to provide backwards-compatible API upgrades[0]. [0]: https://avtok.com/2014/11/05/interface-upgrades.html https://avtok.com/2014/11/05/interface-upgrades.html
- travisjungroth 5y agoSwitching to Python examples from Go, but I believe it all applies just the same. > What does "use the same method signature but with different meaning" mean? .draw(times: int) In your code that adds a shape to the canvas. In my code it adds a card to your hand. > What is my "duck typing code"? You collect everything with the draw signature as options to draw with. > Is duck typing part of your API? What do you mean “your API?”. Maybe I mention it in the docs, maybe I don’t. But that doesn’t really matter because if we’re going to compare intent, then this is subjective. And it really doesn’t matter because duck typing isn’t part of an API, it’s a technique / language feature. By just adding a method, I removed your ability to do something. My point with all this is your definition is lacking. You just shifted the pain of defining “breaking change” to the pain of defining “compatible API”. Which is fine if you’re then going to define that strictly (see Hickey’s talk on SemVer), but you haven’t.
- pgwhalen 5y agoThis is a good point, arguably even fixing any bug breaks compatibility, at least if you're a kernel maintainer. An (in)famous Linus quote: If a change results in user programs breaking, it's a bug in the kernel. We never EVER blame the user programs.
- slaymaker1907 5y agoI don't think this is all that useful of a quote outside of operating systems (and even there I still find it of questionable value). You really need to define how people can use your software (at least at a high level) and receive backwards compatibility guarantees. Even in the Linux example, kernel modules do not receive compatibility guarantees because it is difficult to keep. You may need to rewrite your module depending on how it is written when upgrading the kernel. It also doesn't apply in case of security vulnerabilities and certain classes of bugs. Technically viruses can be user programs which rely on those vulnerabilities and even excluding those, there are cases where some API seems benign but later turns out to be flawed (like precise timers in the context of browsers).
- noctune 5y agoI think it's a difference in what one considers to be "the API". Linux is very much to the de facto API side, whereas some other project might be more one the de jure API side with a rigid specification and allowing any change within that. Most things are probably somewhere between the two.
- Macha 5y agoAnd yet even the kernel deprecates and removes things like legacy architectures, filesystems, etc.
- int_19h 5y agoFor some examples of how complicated determining what is "breaking" can be in general, here's a StackOverflow wiki that I created a long time ago to document this kind of stuff for .NET: https://stackoverflow.com/questions/1456785/a-definitive-guide-to-api-breaking-changes-in-net https://stackoverflow.com/questions/1456785/a-definitive-gui...
- Vinnl 5y agoI'd define it as 'everything you've documented so far still works as documented, and possibly if you're aware that many of your downstreams are relying on some undocumented behaviour, you try to account for that too (but that part is a nicety, not a commitment).
- dmitriid 5y ago> Nuance matters. Stating to "just be strict about semantic versioning" doesn't help, semver is fuzzy. That's why I prefer ComVer: https://gitlab.com/staltz/comver https://gitlab.com/staltz/comver
- paskozdilar 5y agoComVer doesn't account for changes that don't change the API. For example, assume that you're using a dependency, libparser, v1.0, which uses ComVer. In version v1.1 they add a new function, foo() which does something that you want. However, at the time of you writing the software, the latest version was v1.2, which you use because you're a good engineer who likes to keep things up-to-date. Now think about what happens when you distribute your application to three people: - person 1 has libparser v1.0 - you program can't work, because v1.0 doesn't have foo(), so you obviously must require minor version that your program was built with - which is v1.2 - person 2 has libparser v1.2 or higher - you program works. - person 3 has libparser v1.1 - you program CAN work, since v1.1 contains foo(), but it WILL NOT work, because your application requires at least v1.2. Now, you might say, "well a developer should know that foo() was added in v1.1 and pin that as the lowest required vesion" - in which case I reply that ComVer doesn't help you a tiniest bit with this, because both API extensions and bugfixes are mashed together into a minor version, so now you have an order of magnitude more changelogs to go through. SemVer stricly separates API extensions and bugfixes so you can just browse the minor version changelogs and see which version added the feature you require.
- slaymaker1907 5y agoNo, it really shouldn't be. It makes it impossible to distinguish between those "theoretically this a breaking change" kind of changes and the "you're going to need to rewrite a bunch of code" kind of changes. At the very least libraries should be allowed to define how the library must be used in order for their semantic versioning to apply. For languages which allow "import * from x" style imports, the library should still be allowed to add in new functions without that being a breaking change. If you want your software to be pristine forever, you really need to pin your dependencies, ideally via copying the source of the library into your repo so you aren't reliant on a package manager being available in the future. For library developers, regardless of versioning scheme you need to avoid ANY breaking changes whatsoever. Instead of changing an existing function, introduce a new one so that your downstream users can still access the old behavior while keeping up to date. Trust me when I say this will be much easier most of the time than patching old versions with security updates and bug fixes.
- paskozdilar 5y agoAPI is API. Either a library implements it or not. There is no room for "just this small API change uwu" in large-scale development.
- usrusr 5y agoThat way the land of "n.0.0" versioning lies, where only the major is ever incremented, even for changes that won't affect anyone because the code that would be incompatible hasn't been written yet. At least for typed languages where an incompatibility clearly caught at compile time is usually preferable over a murky "technically it's still the same API, but..." I'd rather have separate numbers for major incompatibility/minor incompatibility.
- paskozdilar 5y agoIncompatibilities happen, APIs evolve. It's a fact of life. Semver helps communicate those incompatibilities to people who care about it. If you don't want to freeze your API, then just use 0.X.Y, everyone will understand that they need to do regular maintenance if they want to use your code. But please, I beg you, do not use version 1+ if you're not planning to keep the compatibility promise.
- Groxx 5y agoThere absolutely is fuzziness in incompatibility. I can change one part of an API and leave another untouched - that's part compatible, part incompatible. It's only an issue if you used the changed part. (If you think that first one always counts... what if the changed part is literally called YouMustNotUseThisFunctionOrYourCodeWillAlwaysBreak() ? It's clearly implied to not be part of your intended API, despite technically being part of it.) I can add something to a type, in a way that's backwards compatible at compile time... but common reflection patterns might cause everyone's code to explode. I can make a change that solves a bug that someone was accidentally relying on by doing the wrong thing, but doesn't affect compile-time behavior, nor runtime for anyone using the library the way they should. But that bug-user's code is now broken, is this an incompatible change?
- ithkuil 5y ago> I can make a change that solves a bug that someone was accidentally relying on by doing the wrong thing ... There is even a "law" for that: https://www.hyrumslaw.com/ https://www.hyrumslaw.com/
- paskozdilar 5y ago> I can change one part of an API and leave another untouched - that's part compatible, part incompatible. It's only an issue if you used the changed part. When you commit to a version 1, you assume that every user of the package is using every feature you provide through your official API. If you break any slightest piece of the API, you've broken compatibility. It might "kinda work" for many users, but it will almost surely cause significant pain for many others. > I can make a change that solves a bug that someone was accidentally relying on by doing the wrong thing, but doesn't affect compile-time behavior, nor runtime for anyone using the library the way they should. But that bug-user's code is now broken, is this an incompatible change? It is not an incompatible change, and it is the responsibility of the bug-user's code to fix the bug in his program. Of course, when such a thing happens on a large-enough scale, the API developer sometimes cannot afford to "fix" the behavior and force countless users to fix their programs, so the quirk just becomes de-facto part of the API.
- 5y ago
- viraptor 5y agoThere's an extra dimension here though - support. Projects don't have unlimited resources which in majority of the cases means that only one major version is live. For downstream consumers that gives 2 options: get stuck on an old version silently sometimes, or deal with an occasional breakage during usual dependencies updates. If the old version is used for talking to some external service, you will break one day.
- paskozdilar 5y agoGood point. However, in case that two versions are so incompatible that both sides of communication channel need to update, then I think it's fair to place responsibility on the service writers to notify their users (through deprecation warnings, documentation, chat, etc.) of the change.
- pcwalton 5y agoWell, that's clearly not the way Go operates. Go makes incompatible changes between minor releases; they just don't break type signatures. For example, debug/pe ImportedLibraries(), which is supposed to return all the libraries that a module imports, was stubbed out to return "nil, nil" in a minor release [1]. This is clearly an incompatible change, but as it didn't cause code to fail to compile the Go team deemed it semver compatible. Edit: Apparently this is wrong! See replies below. [1]: https://fasterthanli.me/articles/abstracting-away-correctness https://fasterthanli.me/articles/abstracting-away-correctnes...
- philosopher1234 5y agoI think taking a highly abstract definition of backcompat is not useful. We need a practical definition of back compat. If there are no (or effectively no) downstream consequences of a change, it is clearly backcompat. If there are some downstream consequences, you get into judgment call territory, but it still may be worth it. We cannot create a perfect universal rule here, and Amos is a fool for holding that standard so rigidly.
- pipe_connector 5y agoIt's worth noting that the author of that article was mistaken, there was likely some other issue with their software than what they described here. ImportedLibraries() in the pe package has never done anything other than returned nil, nil. This wasn't changed in a minor release. You can browse the source history here: https://github.com/golang/go/blame/master/src/debug/pe/file.go#L439 https://github.com/golang/go/blame/master/src/debug/pe/file....
- slrz 5y agoFor example, debug/pe ImportedLibraries(), which is supposed to return all the libraries that a module imports, was stubbed out to return "nil, nil" in a minor release [1] I just looked at the Git history and this is plain false. It already looked that way when the big source tree move (src/pkg/ -> src/) was done in 2014. Tracing it back further (to before Go 1.0 times, when there wasn't even a builtin error interface yet and the function returned os.Error), ImportedLibraries was *never* implemented in debug/pe.
- TheDong 5y ago> There is no "kinda incompatible", "99% compatible" - when it comes to dependencies, they either work properly or they don't. > Without being strict about semantic versioning, it is impossible to make it so. There is no semantic versioning if you're that strict. You have to update the major version on every single release, no matter the contents, if your idea of backwards compatibility is that absolute. Let me give you a concrete example: I have a go package with this code: var ErrBadThing = errors.New("somthing bad happened") func DoThing() error { /* maybe return ErrBadThing */ } I notice I had a typo in my error message, "somthing" when I wanted "something". I fix that typo. Is this a breaking change? The answer is "it depends" err := mypkg.DoThing() if errors.Is(err, mypkg.ErrBadThing) { /* will still work after change */ } if strings.Contains(err.Error(), "somthing") { /* will no longer work after change */ } So, is it a breaking change or not? I would argue "no, not breaking, if the user uses the API as expected, it does not break", but you may argue otherwise. If you argue that is a breaking change, well, what about adding a new method, which breaks reflection (like 'reflect.NumMethod()' returns one more, so if someone relied on indexing into your methods with reflection, you broke em!)? What about someone downstream applying a "patch" to your code before compiling it? Any change can break that. The go authors have taken a much less strict approach. Go is still semantically "v1", but they've made a ton of breaking changes, from making `net/http` silently switch to http/2, to changing the semantics of various tls and security related functions, etc etc.
- Quekid5 5y agoYes. This is an inevitable consequence of Hyrum's Law[0]. It's been observed before but I find Hyrum's Law a very good and concise summary of the problem. [0] https://www.hyrumslaw.com/ https://www.hyrumslaw.com/
- VMtest 5y agoIt's a good summary of the problems that exists in any sort of engineering, ultimately, they are actually people's problems
- slavik81 5y agoI would argue that you cannot do semantic versioning in a useful way without an API specification of some sort. For most projects, that would be the API documentation. If the only specification is the implementation, then every behaviour of the library is in the spec and every change is a breaking change.
- mftb 5y agoThis is the answer. It's all internal, he says in his case. They know what they're doing with their own stuff. Staying on v0 is just another signifier that it's one of their internal things, they need to handle specially.
- bborud 5y ago> Then just use 0.y.z versions and be done with it? Yeah, that would work, except a lot of people read 0.x.y versions as "alpha quality". Regardless of the actual code quality.
- gowld 5y agoIf you expect a certain version to be valuable long term, promote it to Z.0 version. It's OK to to have AwesomeButRapidlyChangingLibrary-2022.03.0 branched from 0.y.z.
- bborud 5y agoOr, as some people recommend, skip directly to V2 as the first version.
- philosopher1234 5y agoThis is true and unfortunate, but I think the engineering value of meaningful versions is important and I expect that with enough time people will adapt to understanding a different meaning of 0.x. And besides, people are right to understand 0.x is risky b.c you are not guaranteeing backwards compatibility.
- pgwhalen 5y agoIs this a problem inside a small company though? I would expect there to be much better signals about how alpha-ish a library is in that setting (i.e. talking to your coworkers).
- bborud 5y agoFunny you should ask. Yes, it can be when you use third party software and people have version number hangups.
- pgwhalen 5y agoVersion number hangups are indeed a problem, I don’t mean to suggest my organization has escaped them. But if you can wade through those successfully, the technical solution itself often does make sense.
- pgwhalen 5y ago>> Due to our size, we don’t need any kind of backwards compatibility, we just update everything. > Then just use 0.y.z versions and be done with it? FWIW, I also work in an organization that thinks of libraries this way, and we've found success and simplicity in versioning (for production) our Go libraries as 0.X.0 where X is just a monotonically increasing number generated by the build pipeline.
- mananaysiempre 5y ago... Zerover[1]? [1] https://news.ycombinator.com/item?id=28154187 https://news.ycombinator.com/item?id=28154187
- pgwhalen 5y agoYep! I think it's fair to needle open source software, but it absolutely makes sense for a lot of internal development to adopt this sort of versioning policy.