And C# uses "@keword".
This is definitely a key concern, and it's one of the reason that I favor something like epochs -- that is, a kind of "big release" that groups together a number of changes. This makes it easier to categorize code samples and things as "older code". It also means you can label stackoverflow answers with a simple epoch number to give context and so forth.
I think another big factor here of course is error messages. That is, if the problem is the code not compiling, we can presumably recognize patterns from older epochs and give a very helpful error (much like we used to do when removing obsolete syntax etc).
Can we have epoch code names (like android/ubuntu)?
This feature will become available in the bronze epoch.
We could name the epochs after different crustaceans, then have different logos (also like Android), so thereād be a Crayfish Ferris and a Lobster Ferris and a Fiddler Crab Ferris and aā¦anyway.
Some serious points:
I believe a large portion of the perceived instability of Rust simply comes from the amount of Rust code thatās still stuck on unstable, and the unavoidable fact that no oneās had Rust code in production for several years yet. Iām not sure thereās a clear action item there, other than maybe trying to reduce the number of features in āunstable limboā, or trying not to design and implement features that we arenāt going to be pushing to stabilize in the relatively near future (or are needed to make progress on stabilizing other stuff).
I completely agree that languages like C++ do make plenty of minor breaking changes in practice, and that āepochsā as described here are pretty much exactly what every other serious language seems to be doing. Iām totally on board with the idea of doing something similar for Rust, especially if the ābreaking changesā are as minor and/or desirable as the examples given so far.
But I did have one concern about the impl/bare/dyn Trait syntax example (which I would love to see actually happen btw). For features which have a significant impact on the public APIs that libraries present to their clients, I think itās desirable for there to be one syntax that libraries can use such that none of their clients, on any past or future epoch, get warnings, errors or bugs. In this particular example, I think achieving that is as simple as not deprecating the āimpl Traitā syntax, which seems fine to me since the plan is merely to make the āimplā keyword redundant, not to repurpose it after weāve redefined bare Trait.
Iām not sure how much experience people here have writing Haskell, but a problem that has been growing and growing and growing lately is this at the start of every file:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE DeriveGeneric, DeriveFoldable, DeriveTraversable #-}
{-# LANGUAGE FunctionalDependencies, MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
Now thatās not to say that language extensions and #[feature]
are a bad idea. But they do post the risk of ending up, in a couple of years, with every non-trivial project starting with a list of a dozen features and changes etc. etc.
Has Perl-style use v5.20
been considered?
This is the epochs proposal.
I think this point is quite valid for things like adding new keywords (catch
etc.), and probably the change to match
semantics as well, but... less so for changing the meaning of &Trait
(or banning it or whatever). Not sure what difference this makes to your argument, just wanted to point it out.
I donāt agree. We can change the meaning of a syntax by doing a āpivotā - first we ban it and replace it with a more explicit form (ādyn Traitā in this case). This is a mechanical change.
Then at some point after the epoch has started, when most code is moved over, we introduce a new meaning for the syntax.
Even if we didnāt wait, running rustfix
would still be a mechanical change (because rustfix knows what epoch youāre in, so it knows what that syntax meant). Thereās just a risk if you try to do it manually.
Iā¦ wasnāt disputing any of that. My point was that itās not the sort of thing they tend to do in new versions of the C or C++ standards.
That might be true, but to me the difference isnāt really āwhat C++ has doneā vs āwhat Python has doneā but āwill people dread upgrading across this?ā If its a purely mechanical change, the answer to me seems to be no.
There are a few things Iād like to have clarified to make sure weāre all on the same page.
1. Will each crate specify its own epoch, or will the root project specify the epoch and its dependencies inherit it?
So far, Iām assuming the former, because the latter would imply that libraries would have to be written in such a way that they are compatible with every epoch that they decide to support. If itās the former, thenā¦
2. What kind of changes can and cannot be made in a epoch?
or the collorary:
2a. If a library updates its epoch, is it (allowed to be) a breaking change for the library (i.e. does the library need to bump its major version)?
If the answer is yes, then library authors need to be aware of this. Bumping the major version means that applications on an older epoch will be able to continue using the older major version of a library without fearing that cargo update
will update the library to a version that is incompatible with the applicationās epoch.
I think we already have this issue today with Rust releases: if a library starts using Rust 1.y features without a major version bump, and an application that uses that library is stuck on Rust 1.x, where x < y, then cargo update
will update the library to a version that is not compatible with Rust 1.x and it will not compile. If we eventually logic to cargo update
to not upgrade a library to a version that requires (based on an attribute added in Cargo.toml
, perhaps) a newer version of Rust than the currently installed version, then we could do something similar for epochs to avoid imposing major version bumps on library.
The answer to this question will have an impact on the answer to the next question (specifically scenario 3.2):
3. What is the interaction between crates using different epochs?
There are two scenarios. Letās suppose application A uses library B.
3.1. Application A uses a newer epoch than library B. 3.2. Application A uses an older epoch than library B.
In scenario 3.1, library B might be using features or syntax that has become deprecated or that changed semantics in the epoch that application A uses. If these changes donāt affect the libraryās API (e.g. the proposed change to match
), thereās no issue. But if they do, then in order for this scenario to work, there needs to be an alternate feature or syntax that application A can use in order to use library B.
For example, letās consider the semantic change to &Trait
. Suppose that library B defines these traits:
pub trait Foo {
pub fn foo(self, x: &Bar);
}
pub trait Bar {}
If application A wants to implement Foo
for one of its types, it will be able to, but it will have to use the new syntax:
impl Foo for MyFoo {
pub fn foo(self, x: &dyn Bar);
}
(If it used &Bar
, it would fail to compile because the declaration wouldnāt be compatible with the traitās.)
This might be a problem if library B exports a macro that expands to an impl Foo for $x
: presumably, the macro would be written with the old syntax. If you use that macro in application A, which epoch should the expanded code be compiled under? In order to maintain compatibility, weād have to compile the expanded code with library Bās epoch. I donāt know if thatās easy of even feasible.
Scenario 3.2 is essentially scenario 3.1 backwards, but now instead of worrying about forward compatibility, weād be worrying about backward compatibility. If new epochs are allowed to introduce features that code using an older epoch cannot consume, then it may force applications to upgrade their epoch in order to keep using the library. Coming back to question 2, if a library updates its epoch and has the consequence of forcing downstream crates to update their epoch, then thatās a breaking change. Now, if the library doesnāt use any of the features that are backwards-incompatible, then itās not a breaking change. If the library then evolves and adds a new feature that depends on a feature only available in the new epoch, it will force downstream crates to upgrade only if they want to use that feature.
Random thoughts from IRC:
I would like our current epoch, the default epoch for code without epoch markers, to keep updating at a reasonable pace. I propose that every LTS is a good pace; perhaps we switch the epoch over on LTS releases, so that people can use a stable compiler with the latest standard.
So Rust doesn't currently have a Long Term Support version planned, and from what I understand of this proposal, epochs are not the same as LTS releases either. Is that correct? Would the rust compiler (or anything in the ecosystem) ever get patches affecting an older epoch? (I would assume yes because bugs happen, but how would this work and be communicated?)
If we're supporting every 1.x version on every epoch on every platform, what does that do to the CI build matrix over time? Or rather, obviously it adds another dimension, but is that sustainable?
Is having an LTS version under consideration currently as well, and if so, how do yinz envision LTS and epochs interacting?
I think LTS and epochs are separate ideas. LTS is about maintaining older version of the compiler; epochs are about making breaking transitions in new compilers without losing compatibility with older code. The LTS compiler doesnāt need to worry about any features/epochs that came after its release, and the new compilers should continue supporting old code regardless of the existence of LTS.
Yeah, this says nothing about LTS. I'm still very interested in an LTS release.
So, with the "epochs == flags" idea, older epochs would still be getting patches, that is, there's still only one compiler, and it knows how to build all epochs. So any fixes for stuff not specific to epoch-related flags is gonna get improved on all epochs. Stuff that new flags replace, on the other hand...
I think this is a good, and very important question.
https://isocpp.org/files/papers/p0636r0.html
Hereās whatās different from C++14 -> C++17. Note removed or deprecated features, for example.
C++ standard explicitly permits implementations to support removed library features (and language features too in some cases), which the implementations are surely going to do for years. Rust, unlike C++, is an implementation, so the strategy taken by C++ is not of much help to it.
Part of the proposal is that newer Rust compilers would continue to support older epochs, otherwise āepochsā would be no different from a major version bump (itās even been suggested that Rust would never need a major version bump if epochs happened), so C++'s strategy is very relevant.
There are uses for stable features besides breaking changes too. You might want to disable non-lexical lifetimes for teaching purposes, for example.
Ah, finally found the epochs thread. Overall, Iām very glad this is happeningābreaking changes to the surface langauge are inevitable. The use of years not versions strikes me a bit as a marketing gimmick, but I always avoided python and thus donāt have scars from itās 2 to 3 transition.
One detail, that I assume will be fleshed out but would like to kick off discussing now, is the pipeline of unstable features into epochs. Currently all our unstable features are non-breaking, but with epochs we can start have breaking unstable features. Iām thinking the nightly should have a ānext epochā to queue up breaking unstable features we are considering stabilizing.
Less clear to me is how this works with the āall errors were warningsā rule. Do we need two next epochs? Separate warn and err versions of breaking unstable features?
Put differently, maybe itās better to keep things simple on stable, but nightly will have to do much of what this RFC proposes either way.
One other question worth bringing up soon-ish is how libraries are supposed to support multiple epochs, if at all. I canāt think of any option besides the simplistic #[cfg(epoch=...)]
answer, but we clearly need to decide if this is the sort of code we want people to write when impl Trait finally stabilizes:
trait MyTrait {
// speaking of marketing gimmicks...
#![cfg(epoch = "hermit-crab")]
fn foo() -> MyIterType {}
#![cfg(epoch = "mantis-shrimp")]
fn foo() -> impl Iterator {}
}
We should probably make an actual epochs thread at this point.