I'm curious, do you have a language in mind that has been better than Rust as far as breaking changes go after the 1.0 stable release? An example? Rust has been far above most languages/libraries that I've found in dealing with breaking changes. The complaint here seems pretty empty to me. (Complaining for the sake of complaining)
I think that Java has (on the whole) been very stable, but as steveklabnik pointed it, it also has had breaking changes. But considering Java's age and the quantity of these changes, I'd say it's been exceptionally good (especially since there's some fairly hefty pre 1.5 stuff that will still compile and run.) Clojure, Go, JavaScript, and Scheme come to mind.
I don't think it's fair to compare a language's breaking changes to a library's. I expect different levels of stability depending on the library, but with languages I expect the same level of stability. Rust __has__ been very good at dealing with breaking changes, yes. I'm stating that I don't like breaking changes in a language. Breaking changes in a 1.0 > language are something that should be exceedingly rare.
It might appear differently because we're very up front about any change that might possibly break any code, even theoretically. We don't make any changes that we think actually break code, except for blatant bug fixes.
Go has made changes post-1.0 that were more aggressive than anything Rust has done, such as changing the size of int.
>Go has made changes post-1.0 that were more aggressive
>than anything Rust has done, such as changing the size of int.
Changing the size of int shouldn't be a breaking change in Go since pointer arithmetic isn't allowed and everyone should use fixed-size variables when you're relying on this behavior.
Even if this isn't true, Go's release note says:
>The language allows the implementation to choose whether
>the int type and uint types are 32 or 64 bits. [1]
The only thing that changed was the implementation, not the language.
> Changing the size of int shouldn't be a breaking change in Go since pointer arithmetic isn't allowed and everyone should use fixed-size variables when you're relying on this behavior.
All of the "breaking changes" in Rust have been of this form.
We're very up front about any possible breakage. When we talk about "breaking changes", we mean "a change that shouldn't be breaking, but could be breaking if code was relying on this bug/implementation-specific behavior". Those types of changes—changes that might cause breakage in practice but do not change the language definition—are often not considered "breaking changes" in Go. But in Rust we often call them "breaking changes", because we are concerned first and foremost about the practical considerations of changes we make, not just whether we are technically allowed to make them per the letter of the law.
> Changing the size of int shouldn't be a breaking change in Go since pointer arithmetic isn't allowed and everyone should use fixed-size variables when you're relying on this behavior.
Theoretically one should be using fixed-sized variables, but people may not realise they're relying on a particular behaviour. One of the whole points for using a language like Go over, say, C is that humans are imperfect and so having the computer assist is good, and this imperfection penetrates into all aspects of the software process. With a tasty name like "int" and being generally the default type, I'm sure a lot of code uses it when fixed sized types may be better.
> The only thing that changed was the implementation, not the language.
This is somewhat irrelevant: it still results in code not compiling or possibly changing behaviour, because people in fact have to use an implementation of Go, they don't write in an abstract idealised version of it. Rust's breaking changes are generally the same sort of thing (undocumented implementation details, changing the implementation to match the long-stated desired behaviour and bug fixes), but they still result in people's code not compiling or changing behaviour and so need to be handled as such.
I'm sorry to say but you must be delusional. Can you list the these `aggressive` breaking changes? There were only 7 releases since go1 and I fail to find any breaking change in the language specification. On the other side each Rust release has a fat list with "BREAKING CHANGES". Many Rust packages only work with specific Rust versions(i.e. nightly). The rust ecosystem(std lib, tools etc) is also way behind Go in terms of stability. Why do you need rustup if Rust is so backward compatible? I'm not saying that Go is a better language than Rust but the its ecosystem and dev experience is definitely superior.
> Can you list the these `aggressive` breaking changes?
Changing the size of int. Changing methods to introspect the type of their arguments and do things differently. And so on.
> There were only 7 releases since go1 and I fail to find any breaking change in the language specification. On the other side each Rust release has a fat list with "BREAKING CHANGES".
Because we have a very specific definition of "breaking change" that is primarily concerned with the practical effect of changes we make. Go does not consider these changes "breaking". Using Go's definition (changes to the language definition), we have no "breaking changes".
If we were to change the size of int (something we will not do, by the way, due to the practical effects of making such a change), then we would list it under "breaking changes", even if we were technically allowed to do it. That's because we care about the practical effects of our changes, not just the letter of the language definition.
> Many Rust packages only work with specific Rust versions(i.e. nightly).
Because they are explicitly opting into unstable features that are carefully marked as such. We can't stop packages from doing that. Nor can any other language.
> The rust ecosystem(std lib, tools etc) is also way behind Go in terms of stability.
The parts of the Rust standard library that are marked stable have remained completely backwards compatible, in both interface and implementation.
> Why do you need rustup if Rust is so backward compatible?
Because it's nice to keep your compiler up to date and to target different platforms?
You are right about the compiler changes but even so you can't compare 1-2 compiler BK with Rust which has language changes as well. Compiler changes are the norm in Rust.
>> The parts of the Rust standard library that are marked stable have remained completely backwards compatible, in both interface and implementation.
Rust doesn't have backwards incompatible language changes.
> This looks like a breaking change on a stable API.
No. It is a method addition. It is breaking only in the sense that code that didn't explicitly invoke the previous "as_ref" method might call this new method instead. It's the moral equivalent of:
type A struct { ... }
type B struct {
A
}
func (a ﹡A) Foo() {}
and then a later version of Go adds a method:
func (b ﹡B) Foo() {}
Such that code that called Foo() on an instance of ﹡B might call the new method instead. Go can make those changes.
Compiler changes are the norm everywhere? I'm not sure what you're trying to say here.
> This looks like a breaking change on a stable API. Am I wrong?
Yes and no.
Rust's policy on breaking changes is that changes that can be fixed by properly qualifying an implicit path are not breaking. Otherwise, adding any method to anything would be a breaking change. In this case, you can use the UFCS syntax to disambiguate.
So it's an "allowed" kind of breakage because not allowing this means freezing the stdlib.
(Go doesn't have this issue due to lack of generics, overloading, and interface-based overloading. Edit: actually, go does too, due to inheritance, but that is easier to avoid and isolate. In rust you can always write client code that breaks if the stdlib adds a method, anywhere. This is true for most typed languages).
Anything that has the chance of practically breaking things is still run through crater (which tests impact on the ecosystem) and as you can see that PR had minimal impact.
Steve has pointed out that go did have this issue as it has a limited level of auto-deref, which it has changed in the past: implementations performed two levels of auto-deref when executing methods when the spec only requires one, the implementations were changed to only allow a single auto-deref: https://golang.org/doc/go1.4#methodonpointertopointer
Right. However, as I mentioned in the edit, you can still be careful about avoiding breaking changes through inheritance and autoderef in the evolution of Go's stdlib. It forbids very specific types of methods from being added, and if you avoid that, you can continue to add methods as if nothing is wrong.
Rust (and C++, because SFINAE, and many other languages), on the other hand, technically has a breaking change each time any method is added to any public type in the stdlib. It's always possible that the client lib was using a trait method of the same name, and now has to fully qualify the method call.
https://doc.rust-lang.org/reference.html , which is accurate, but not always 100% up to date with the latest RFCs. There's also work on a formal, proven specification of the memory model, but that's not done. It'll be a while.
That's understandable. I've just tried to prove the point that Rust is still a language in flux compared with Go, hopefully making the Rust team aware why some developers hesitate to use Rust on new projects.
> I've just tried to prove the point that Rust is still a language in flux compared with Go, hopefully making the Rust team aware why some developers hesitate to use Rust on new projects.
That's not what I've seen from your comments. Instead I've seen some confused arguments about what "prose only" means (anyone in the PL field would consider both Rust and Go's documentation "prose"), combined with incorrect statements about both Rust and Go and a completely baseless assertion that Rust is "a language in flux".
Rust uses a much stricter definition of breaking change than does Go. As discussed elsewhere in this thread, Go changed the size of integers. While this is technically allowed by the language (it wasn't previously specified), and it shouldn't break conforming code, it can break code that depended on the previous size.
The Rust maintainers would have considered this a breaking change. The Go maintainers did not. This isn't to say either side is right or wrong, just that they are measuring different things.
Additionally, the Rust maintainers have been exceedingly cautious whenever making these types of changes. They literally download, compile, and test all published crates to look for indications that such a change might actually break existing code. In the very few cases it has, they've worked with crate authors to incorporate fixes.
The very low bar Rust sets for determining what is a breaking change directly reflects the extreme regard they have for this issue.
> Why do you need rustup if Rust is so backward compatible
To cross compile. To have quick toolchain updates. To get bleeding edge compiler improvements (e.g. speed) quickly. To test out new features. To help find bugs in the compiler.
One very common use case of rustup is to use clippy. Clippy is a developer tool which hooks directly into the compiler and uses all sorts of private APIs, an inherently unstable thing. It only works on nightly. Lots of people write their code to work on stable, but want to use this tool so they use rustup. Note that no language has a stable way of hooking into the compiler.
Very few rust packages only work with nightly. Care to provide some examples?
> Why do you need rustup if Rust is so backward compatible?
For one, testing on various versions of Rust. For example, I have a kernel project that's pinned to a particular nightly version, while the rest of my projects build on stable. Rustup makes this Just Work.
Well, that's my point! You shouldn't need to test various versions of Rust if you there is a strong backward compatibility policy. I might be mistaken but my feeling is that most of the rust devs are using the nightly version thus the reason of a tool to debug/test different versions.
He didn't say he was "testing" with nightly: some experimental features only work on nightly (which, being experimental, the features may change in breaking ways, but that's why people have to opt-in to using a nightly) and so if one of your project needs one of these features, you can use rustup to get nightly for just that project and the stable releases for the rest of your work.
A staged release cadence with different levels of surety gives people the ability to play with features as they're developed to make sure those features solve the problems they're trying to solve (in the best way) by giving time for real-world experimentation and feedback. A feature can graduate from nightly-only to stable, and it then has a strong backwards compatibility requirement. The nightly experimentation period is valuable to get those features perfected before people can start relying on them more broadly.
What if I would like to guarantee this property for my own code? As well as testing each nightly as they come out, in case something accidentally breaks, so it can be fixed before a release? This tooling assists greatly with that.
(And dbaupp is correct that it's not always about testing; not all of the OS dev features are in stable yet, so nightly is the only option for that kind of project.)
Right, but humans are fallible. Bugs happen. It's a good idea to test early and often, just to make sure: Travis runs are extremely cheap. Better to catch accidents before they make it into an actual release. More testing doesn't hurt anyone.
I would like the code developed now to work with all subsequent rust releases until 2.0 so that I can take advantage of improvements to the compiler and std libraries without any additional effort.
A small effort may effort may be required if there were security/critical bugs.
> I would like the code developed now to work with all subsequent rust releases until 2.0 so that I can take advantage of improvements to the compiler and std libraries without any additional effort.
> Although we expect that the vast majority of programs will
> maintain this compatibility over time, it is impossible to
> guarantee that no future change will break any program.
It's extremely similar to our attitude, and that of Java, etc:
> Of course, for all of these possibilities, should they arise,
> we would endeavor whenever feasible to update the specification,
> compilers, or libraries without affecting existing code.
This isn't a bad thing! My point is as I said below: every language includes some small breaking changes, even if the goal is to have very, very few of them.
Sure. My point is that this is similar to what we guarantee: the compiler used to compile some code, and now it won't. As they say, "It has therefore been disallowed in Go 1.4, which is a breaking change, although very few programs will be affected." Our breaking changes have been similar.
Rust has rfcs, and many of the "breaking changes" are in places where the implementation didn't follow the RfC. Others were in things that were never intended to compile. Its effectively the same thing.