My experience with the Rust 2018 preview

Recently, I wrote a little a side project to sign git commits without gpg. When I did this, I decided to use the Rust 2018 edition. I also transitioned an existing library from Rust 2015 to Rust 2018 to see how that tooling worked. I thought I’d write a blog post about my experience using the Rust 2018 preview and the state of things right now.

Module changes

The main thing I noticed vividly was the new experience of the module system. Using paths is one of the most pervasive aspects of programming - after all, everything has a name - but one that you don’t notice most of the time. The change in how Rust’s paths work was, in my opinion, significantly for the better.

The first thing I noticed was the big “interruption avoidance” win from not having to write extern crate declarations. My project was one of those little CLI applications that end up accumulating a lot of dependencies. I use vim & I use cargo-add; all I had to do to add a dependency was :!cargo add ${crate name} and go back to what I was doing, no opening other files or even losing my cursor position. Since external crates are always in scope, I could just type :!cargo add hex, and then type hex::encode( immediately, no matter where I was in my project.

This also had a big impact on my experience as soon as I started copying code from my lib.rs into new submodules. In Rust 2015, you can do something like hex::encode( in the lib.rs without using any use statements, but you can’t do it in any other modules. It was great to that external dependencies were treated consistently - especially std - across all the modules of my project.

One thing that felt unfortunate to me though was the state of macro imports with these changes. When I wanted to get a macro from one of my dependencies, I had to add #[macro_use] extern crate to my lib.rs. This was especially unfortunate when I started using the macro well after I had already started depending on the crate. I tried to call bail! midway through changing my error handling and my workflow was disrupted by having to go make it so I could use macros from failure. I hope we can get macro importing working very soon.

The other big downside I experienced had to do with crate renaming. One of my dependencies - ed25519-dalek - has a very long name. In 2015, I would do extern crate ed25519_dalek as dalek; to shorten it, but that doesn’t work well in 2018. Instead, I had to add use ed25519_dalek as dalek; to the top of each module I used names from that crate in.

We have a cargo feature to resolve this: instead of adding the extern crate statement, I would have to add this to Cargo.toml:

[dependencies.dalek]
package = "ed25519-dalek"
version = "0.7.0"

However, I ran into two different bugs while trying to use this feature:

  1. rustc currently ICEs if you rename a dependency that you have a transitive dependency on as well.
  2. If a dependency is optional, when you rename it, the feature still uses the package name, not the name that you gave it.

We definitely need to get this renaming dependencies feature ready to stabilize for the 2018 edition, otherwise renaming a dependency will be much worse than it is today.

Overall, I’m excited about the direction of the module changes. Speccing out this project, my workflow was much less disrupted every time I needed to add a dependency or move code between files. It needs a bit of polish still before its ready to ship, but that’s to be expected.

Match ergonomics

Match ergonomics have been stable for a while, but I continue to find them paying dividends. I’m sure there are times where I’ve used it without even noticing it, but there were many cases where I did notice. My experience usually went like this:

  1. I write some code that looks good and feels right.
  2. I pause: this isn’t right; I need to go back and add a ref binding or something like that.
  3. Wait, no this works! It will compile exactly they way I expected it to.

Getting to skip a trip through the edit/compile/debug cycle has been great, now I just need to get my brain to start accepting the code I write as well.

One thing that was annoying, though, was that I still can’t match an Option<String> with a pattern like Some("string"). This lead to fidgetty, opaque solutions like match expr.as_ref().map(|s| s.as_str()). What’s worse is that later I wanted to do move the value in one of the other branches, so I had to completely rewrite the match statement to accomodate the issue in a different way (instead doing Some(ref s) if s == "string").

I hope in the future we can address matching String against string literals and Vec against slice literals - as well as matching through smart pointers like Box and Rc and Arc; there are problems with solving this in a maximally general way - like inserting implicit derefs - but I think a less general solution would be justified here.

Transitioning 2015 to 2018

My initial experience trying to use cargo fix was frustrating: it didn’t seem to work at all! I was very concerned, for a few minutes, about the state of our tooling for transitioning between 2015 and 2018. Then, I realized that I was using it wrong, and it actually worked great! Once I got it to work, I ran cargo fix on my code and it became idiomatic 2018 code, with nothing ugly in the diff at all. I was very satisfied.

However, I did have that trouble figuring out how to do it. The problem was that there are three steps involved, and it is essentially that you do them in the right order:

  1. You add #![feature(rust_2018_preview)] to lib.rs.
  2. You run cargo fix on your project.
  3. You add edition = '2018' to Cargo.toml.

First I tried running cargo fix first: it did nothing. Then I tried running it third: it barfed repeatedly on errors that I had to fix manually, one at a time.

One thing that makes this more confusing than necessary is that cargo fix warns you when you have a dirty working copy (so that if you revert it you don’t lose any work), but I needed to make my files dirty in the first step. It seems weird to commit just the 2018 preview feature without also running cargo fix in the same commit.

A problem was that I was going off of the rustfix README and Nick Cameron’s blog post, neither of which make the ordering of events here explicit. It turns out that in the edition guide, which I had forgotten about, the steps are clearly listed in order, and if I had been working from that I probably wouldn’t have had any issues.

I hope that in the final version we can make cargo fix into an atomic experience: you run cargo fix on a clean repo and it both changes your code and updates your edition number.

Conclusion

Overall, I am really excited about Rust 2018! I found some UX issues that we need to fix, but I’m more confident than ever in the direction of the release and I think the final product will both be a better version of Rust and an extremely smooth transition story.

My real motivation in writing this blog post, though, is to encourage you to do the same! If you have a side project or anything on nightly that you feel like updating to 2018 or starting with 2018 in, please do and give us feedback on your experience. The more user testing we get on nightly, the more likely the final release will be a success!