Rust 2018: an early preview

You still need the mod declarations, to be clear. The only thing that changes are what filenames work. Removing “mod” entirely was too controversial.

4 Likes

No, the point of having an edition = "2018" opt-in in Cargo.toml (as opposed to only docs and external communication around the project, that rustc wouldn’t know about) is to be able to make breaking changes. For example, the guide lists async as a new keyword in Rust 2018 so you won’t be able to use it as a variable name like let async = something();, but you could in Rust 2015. (The new raw identifier syntax helps mitigate the keyword thing specifically.)

That said, previous editions will be be supported by newer compiler versions “forever”, so a crate that says on the 2015 edition should still compile in Rust 1.74.0. So in that sense, the support for old syntax won’t be removed from the compiler.

2 Likes

No. Also I feel the same way about Swift. But that's kinda beside the point.

(Further: those who know me are probably rolling eyes and ignoring this statement as I'm technically conservative and pessimistic by nature. Feel free to ignore the sentiment, sorry.)

5 Likes

I agree with this – in fact, while converting a crate over to the 2018 edition preview, it’s what I tried first, and was confused why it wasn’t working.

More specifically, one use I’d tried was:

use log::{debug!, info!};
3 Likes

In Rust 2015, you can use pub(crate) to make something visible to your entire crate, but not exported publicly:

In Rust 2018, the crate keyword is a synonym for this:

I must've missed the RFC for this, but this feels a little special-casey to me - can we also write

pub struct Foo {
    in some_parent_module bar: usize,
}

I guess the counterargument would be that allowing use within the crate is more common than the other uses, but honestly, pub(crate) really isn't that much of a burden to type, and is much more self explanatory.

5 Likes

What bothers me quite a bit is that some changes, that I hadn’t heard of before reading this thread/the Edition Guide, seem to optimize for quick writing, while making readability a lot worse.

In particular with in-band lifetimes and lifetime elision in impl if I see something like fn foo(x: &'a Foo) -> &'a Bar I’ll have to check for a lifetime definition of 'a on the impl and, failing that, whether the type that the impl is for is parameterized over a lifetime 'a, to know the scope of 'a.

Similarly T: 'a inference in structs means that I now have to look at each field of the struct to know what the relation between 'a and T is. Granted I’ll probably want to do that anyway, but particularly with multiple lifetime and type parameters I would prefer having this information upfront. As an aside: Does this really work only on structs and not on enums? That would seem like a strange inconsistency.

12 Likes

Ah yes thanks for the correction! This what I was conceptually thinking, but said edition 2048 when I meant Rust 1.99 (or any arbitrarily high number). Appreciated :slight_smile:

I also don’t care much for how internal and external macro imports aren’t being treated uniformly. Couldn’t the crate and super keywords be used to make them uniform? eg. use crate::baz;, use super::foo; (or use crate::baz!;, use super::foo!;)

All imports in 2018 code will be “absolute” – in this case, we’re importing from an external crate so they must start with the crate’s name, not crate which would indicate that the macro comes from the local crate, which isn’t the case.

I am surprised the doc includes stuff that can be done with Rust 2015. Why not limit it to stuff that can only be done with Rust 2018 (i.e. breaking changes)? That would make things more clear.

2 Likes

Yes, the edition system conflates two aspects and gives them the same name:

  • (Minor) breaking changes opted-into by edition = "2018" in Cargo.toml
  • A “rallying point” (term from the RFC) with a number of new features that together change idioms, a good time for people who haven’t looked at Rust in a while to try it again, etc. Something less frequent and so hopefully more exciting than yet another 6-weeks cycle.

This appears to be by design from the core team, despite repeated objections that it will most likely be confusing.

5 Likes

I have a few comments regarding the new edition:

  • never_type: what happened to this feature? I remember that it was being stabilized and then got pulled out / reverted at the last moment due to some regressions for a few crates. I have not seen any news about it since then. I think this is a very important feature for the language and one I am personally looking forward to. I tried going through the relevant github issues/PRs about it to see if there has been any update since it was reverted (any plans to resolve the issues and stabilize it?), but did not find anything. What’s the plan/status?

    • Since the only problem with this feature (at least in my understanding) seems to be that it breaks a few crates, making it backwards-incompatible, perhaps it could just be enabled for the new Rust2018 edition only? People are going to have to fix breaking changes when they decide to opt into the new edition anyway.
  • Please require the bang for macro imports / use statements, to make a clear distinction from other language items. use some_crate::thing; should import a function or type by the name thing, not a macro. use some_crate::thing!; should import a macro, not a language item. I feel that macros are sufficiently different from other parts of the language that such a distinction is important. It makes everything much clearer.

  • I prefer pub(crate) over crate as a visibility specifier. I think it is much clearer and less confusing. Also, I don’t remember seeing discussion about this. This feels like yet another change being added to the language without sufficient discussion. I have a feeling that this might lead to controversy similar to what happened after impl Trait in argument position was stabilized and people felt like they had no say.

    • There is a nice consistency about pub being the single keyword used to expand the visibility of things beyond private. It also has the benefit of making things more greppable / easier to search. Right now, you can just search for pub to find any non-private items.
  • I also personally dislike in-band lifetimes and lifetime elision. I think it makes the readability of the code worse, as it is more difficult to see where lifetimes come from / where they are defined. I like the current explicitness.

  • dyn Trait: apparently using the old bare Trait syntax will only be a warning in the new edition, not an error? I understand that this is widely-used syntax and will require lots of changes to lots of crates, but it seems like something trivially automatable (with e.g rustfix). I’d have liked to see a single consistent syntax being enforced.

Other than my critcisms above, I love pretty much all the other changes with the new edition. Rust definitely feels like it is becoming even better. Big thanks to everyone involved for your work!

4 Likes

I am also looking forward to the never type, but it is not something that can only exist in some editions. If a function from another crate (which might be in a different edition) returns a Result<Foo, !> value, you have thot value.

Stabilization is blocked on cases let _: Box<std::error::Error> = Box::new(!) compiling correctly https://github.com/rust-lang/rust/issues/49593. What should happen is that new returns a Box<!>, which is then unsized to a trait object. (Error is a trait that ! implements.) What happens today is that ! is coerced to Error (which it can because it can coerce to anything, since it doesn’t have any value) and then Box::<T>::new cannot be called because it requires T: Sized, but Error: !Sized since it is a trait.

This in turn is blocked on people knowing the compiler well enough to change how coercion works being available, but those same people are also in high demand for things like NLL.

6 Likes

I agree that pub(crate) is fine and that changing it to crate feels a bit gratuitous, when it has already been around for a year.

9 Likes

Quoting the edition-guide:

Therefore, if you're using Rust 2015, and one of your dependencies uses Rust 2018, it all works just fine. The opposite situation works as well.

Just to be clear: most features will be available on all editions. People using Rust 2015 will continue to see improvements as new stable releases are made.

I'm having a hard time wrapping my head around how this will work. If a new feature is added in rust version X (where X is from the Rust 2018 edition), how will the rust compiler in version X-1 (where X-n is from the Rust 2015 edition) know how to compile this code?

To test this out, I created a dependency that opted-in to rust 2018 (via the instructions in the edition-guide), and then tried to depend on it from Rust 2015 (rustc 1.26.1) and I got the following error:

error: failed to load source for a dependency on `mydep`     

Caused by:                                                                                                                                                                      
  unknown cargo feature `edition`

Given the first thing I quoted from the edition-guide, I thought this might have [magically] worked. I suspect I have some fundamental misunderstanding (and so I'd like to see the edition-guide do a better job at explaining these compatibility rules)

Editions are separate from versions. A given version of Rust (e.g. 1.34) has a set of language features it supports and a latest edition it supports, but it also supports all earlier editions. So you can still compile a 2015-edition crate with 1.34. (But not future editions the will/did not exist yet when 1.34 will be/was released.)

(This error message means that 1.26 does not know about editions at all, or viewed differently that it only supports crates that are implicitly in the 2015 edition since that’s the default.)

2 Likes

Thanks. that helps my understanding. So in addition to waiting for the first compiler version to support the 2018 edition, we are also waiting for the first compiler to support the 2015 edition (and we can think of all the compilers currently out in the wild as having support from some implicit “zero edition” which happens to be identical to 2015)

Problem here is what "a while" is. This would work if we were doing a 2021 edition (vs. 2018 edition), where new idioms between the 2 editions would be documented, and even that should really only be about those which happen to also break compatibility with the 2018 edition.

The thing about idioms is they change frequently, such that in 2024, even 2015 edition idioms will have changed (supposing people would remain with such an old edition, which will, according to plan, still be a supported setup).

(The plan is to make a new edition every few years, so it is very possible that we’ll have a Rust 2021 in addition to 2018 and 2015.)

@tshepang I think we’re making the same point. We give the same “edition” name to two things: “hey look, these feature are new since the last couple of years” v.s. “you can opt into some incompatibility and get this subset of new features in exchange”. The new features are not the same in both cases, so not having separate terms to talk about them can be confusing.

1 Like

The in-band lifetime:

fn two_args(foo: &Foo, bar: &'bar Bar) -> &'bar Baz

Is easier to read as:

fn two_args2(foo: &Foo, bar: &Bar@bar) -> &Baz@bar

Or something like:

fn two_args3(foo: &Foo, bar: &Bar::bar) -> &Baz::bar
fn two_args4(foo: &Foo, bar: &Bar:bar) -> &Baz:bar

Maybe in-band generics:

fn other_args<'a, T:'a> (v: &'a T, size: int) -> T:'a

To:

fn other_args(T: type@a, v: &T@a, size: int) -> T@a