Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

the 2018 edition changed how i write rust

Looking back at the Rust 2018 edition and the specific changes that quietly reshaped the way I structure code.

Source code on a dark screen

I came to Rust before the 2018 edition landed, in the days when extern crate lines sat at the top of every file and the module system felt like it had been designed by someone who enjoyed watching me fail. The 2018 edition is years old now, and yet I still think of my Rust as before-and-after that release. Not because it added some headline feature, but because of a handful of unglamorous changes that altered how I write code day to day.

Worth being honest up front: most of what made Rust good was already there in 2015. Ownership, borrowing, the type system, the lack of a runtime. The 2018 edition didn't reinvent any of that. What it did was sand down the corners I kept catching myself on, and a few of those corners turned out to matter far more than their patch notes suggested.

The module system finally made sense

The single biggest one for me was the path and module changes. In the old world, getting an item from another module meant memorising a set of rules about when you needed ::, when paths were relative, and where extern crate had to live. I wrote a lot of correct programs by trial and error rather than understanding.

The 2018 edition made paths consistent. You refer to items in the current crate via crate::, and external crates by name without the ceremony of declaring them in every file. The mod.rs requirement relaxed too, so a module could live in a file named after itself rather than a directory full of identically-named files.

// roughly how a project reads now
use crate::config::Settings;
use serde::Deserialize;

mod config;
mod handlers;

fn main() {
    let settings = Settings::load();
    handlers::run(settings);
}

That looks unremarkable, which is exactly the point. I stopped thinking about the module system as a thing to fight and started thinking about it as a way to organise code, which is what it should have been all along.

Non-lexical lifetimes quietly removed a class of arguments

The other change I felt in my hands was the borrow checker getting smarter about lifetimes. Under the old, lexically-scoped model, a borrow lived until the end of its block whether you were still using it or not. That produced a steady drip of borrow-checker errors that were, frankly, the compiler being pedantic about code that was obviously fine.

A diagram-like view of program structure

With non-lexical lifetimes, a borrow ends at its last use, not at the closing brace. Code like this just compiles now, where before you'd have to introduce an extra scope or shuffle declarations around to placate the checker.

fn update(map: &mut std::collections::HashMap<String, i32>) {
    let value = map.get("count").copied().unwrap_or(0);
    // the immutable borrow above is done, so this is fine
    map.insert("count".to_string(), value + 1);
}

The deeper effect was on how I taught Rust to others and to myself. I used to spend a lot of breath explaining workarounds for problems that weren't really problems. Once those errors stopped appearing, the borrow checker's remaining complaints were almost always pointing at genuine bugs. The signal-to-noise ratio went up, and people stopped feeling like the compiler was bullying them.

The ? operator changed my error handling

Strictly the ? operator predates the 2018 edition, but it matured around the same time, and combined with everything else it changed the texture of my code. I went from try! macros and match arms full of early returns to a clean propagation operator that reads like the happy path with error handling folded in.

use std::fs;
use std::io;

fn read_config(path: &str) -> io::Result<String> {
    let raw = fs::read_to_string(path)?;
    Ok(raw.trim().to_string())
}

The honest result is that I stopped reaching for .unwrap() as a shortcut. When propagating an error is one character, the lazy option and the correct option are nearly the same amount of effort, and that nudge matters more than any amount of guidance about good practice.

What it taught me about editions

The broader lesson, and the reason I still bring this up, is that the edition mechanism itself was the clever bit. Rust managed to change syntax and behaviour without splitting the ecosystem, because old and new editions interoperate and the compiler keeps supporting both. I've watched other languages tear themselves in half over a major version. Rust shipped meaningful changes and the worst I had to do was run a migration tool and fix a handful of warnings.

None of these changes were the sort of thing that makes a release announcement sing. No new async, no const generics, nothing you'd put on a slide. But added together they changed the day-to-day feel of the language from something I fought to something I reached for. That's a better outcome than most headline features deliver, and it's why I still think of the 2018 edition as the moment Rust started feeling like home.