loading...

Learning Rust 1: Install things and read a file

jbachhardie profile image Jae Anne Bach Hardie ・8 min read

I've tried to learn Rust multiple times. Unsuccessfully.

I want to be upfront about that because it's a bit hard to admit. I'm a pretty experienced software engineer, mostly in web development but I used to work in data science. I'm curious and stubborn and generally pride myself on picking things up quickly. However, I never studied Computer Science, I never did any serious coding in a low-level language and so while theoretically I read the Rust docs and I understand I've never managed to make it all click in a way which makes it intuitive to reason about.

I'm writing this in an attempt to break this cycle of picking it up, toying around with it, getting frustrated and going back to Javascript. I'm going to set myself up every few nights, pour myself a drink and write about my progress as I learn to solidify my thoughts and keep me honest. I think this might be useful to other Rust beginners (I certainly like reading other people's thought processed) but I'm not experienced enough to be properly didactic. Expect to come on a ride with me while I figure out why my code doesn't compile and hopefully learn to not make my mistakes, don't expect a coherent explanation of what the stack and the heap are. Those are two different words that both mean "a pile of things" and I keep forgetting which is which.

The drink tonight is a French Manhattan, which is where you replace the vermouth with Chambord. I feel it's a misnomer, if anything it tastes less french than its more traditional cousin, all the herbal sophistication replaced with the flavour of blackberries marinated in cough syrup. The cloying sweetness is at least somewhat celebratory.

Anyway, Rust.

The Project

I'm going to attempt to make a version control system. This sounds like a good idea because

  1. It's the kind of thing that can be really big and complex but made out of simple building blocks.
  2. We'll need to build both a CLI and a server, which is neat variety.
  3. Manipulating lots of files performantly sounds like the kind of thing you'd use a language like Rust for.

I might regret this. It's a bit more ambitious than your standard TodoMVC but it's interesting, isn't it? Have you never stopped and wondered how someone starts coding something like git?

Installing Rust

It turns out I already had rustup installed. If you don't have rustup installed, the Rust website provides installers if it isn't already in your package manager. Rustup is itself like a little package manager, it allows you to download different versions of Rust and switch between them. This is the kind of thing I don't have a reason to care about yet but I know from experience is really useful when you're switching between projects which were made with different versions of a language.

I run rustup update to update to the latest stable version. It's been a while since I last updated and I don't want to be caught out by things that might be different from the docs available out there. Fortunately, it's that easy.

When I go to install a Rust plugin for my editor — VSCode, the editor that has taken over even my hardened, spacemacs-loving heart — I notice there's two: the official one and Rust Analyzer. It looks like Analyzer is an attempt to provide a more responsive IDE experience with things like go-to-reference and autocompletion. That's something I always thought was missing from the rust experience in my previous forays, so even though it's officially in alpha I take heart in the 5 star rating the plugin has and brave it. After all, if it doesn't work I can just switch to the regular language server.

Creating the project

The docs say to use cargo new to create a new Rust project. Cargo is, if I understand it right, the package manager for Rust packages, which are called crates (get it?). It's like Rust's npm. I've decided to name my project "ribbit" for no other reason than it starts with "R" and I think it will be funny to type in the terminal, so I type cargo new ribbit and that looks so funny it instantly validates my naming decision. Or maybe the Chambord is getting to me.

Opening the folder in VSCode reveals cargo.toml and cargo.lock files and I've used npm enough to take a good guess at what those are. I actually really like the TOML format, it's much more human-readable than JSON:

[package]
name = "ribbit"
version = "0.1.0"
authors = ["Jae Anne Bach Hardie <EMAIL REDACTED>"]
edition = "2018"

I don't know why "edition" is set to all the way back in 2018 but I decide not to worry about it. Other than that there's a .gitignore file (very useful) and an extremely simple main function in src/main.rs

fn main() {
    println!("Hello, world!");
}

I'm not immediately sure how to run a Rust project but Cargo has been pretty intuitive so far so I type in cargo run and am not disappointed to find it compiles the project in debug mode and outputs Hello, world! to my terminal. So far, so good. I commit everything to a new repository using VSCode's "publish" command that integrates with GitHub. Frustratingly, it creates the repository but doesn't add the origin to my local git and push, which I have to do manually. I guess they're still ironing out the kinks.

Reading a file

Time for the kind of thing that makes me really afraid. Let's see if we can read a file and print it to console. We're going to need to read a lot of files to make a VCS work!

I start by copying the "read a file" example from the stdlib:

use std::io;
use std::io::prelude::*;
use std::fs::File;

fn main() -> io::Result<()> {
    let mut f = File::open("Cargo.toml")?;
    let mut buffer = [0; 10];

    // read up to 10 bytes
    let n = f.read(&mut buffer)?;

    println!("The bytes: {:?}", &buffer[..n]);
    Ok(())
}

To go through this:

  • The use declarations import other crates, in this case the stdlib crates needed to open files.
  • let declares an immutable variable, let mut a mutable one. I love this in theory, in practice I still lack an intuition for when things need to be mutable. For example, I'd think you wouldn't need anything mutable to just read a file and not change it but I think the buffer here needs to be mutable so it can be changed to store the bytes of the file as they come in?
  • & passes something as a pointer, &mut as a mutable pointer. I think. I'm sure we'll get to pointers right now I just know it has something to do with where memory is allocated and that the compiler will shout at me if I use the wrong one.
  • The ? after an operation short-circuits the function to return failure if the op fails. This is how Rust does error handling and it's pretty clever. Functions explicitly return a Result type but you can write them without having to think about that. It's like async function() in Javascript, which will return a Promise despite being written like synchronous code with exceptions, except the exceptions are also typed which is great.

Anyway, this code only reads 10 bytes and then prints them as integers rather than as Unicode so we have some ways to go before we're printing out the whole file recognizably. The standard library gives us BufReader with an interface that makes it easier to read something in as a Unicode string:

use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;

fn main() -> io::Result<()> {
    let f = File::open("foo.txt")?;
    let mut reader = BufReader::new(f);
    let mut buffer = String::new();

    // read a line into buffer
    reader.read_line(&mut buffer)?;

    println!("{}", buffer);
    Ok(())
}

This prints out the first line of the file! We're a while loop away from printing the whole thing, so let's look up how to do that. The documentation is hard to navigate at first, because BufReader doesn't seem to have what I'm looking for. What is happening is that BufReader implements the BufRead trait. Traits are collections of methods that a type implements so BufReader implements BufRead, Debug, Read and Seek. It's BufRead that has the read_line method, as well as a lines method that returns an iterator. That's what we want, because iterators can be looped over with a for loop.

Before we do that, though, I'm starting to realize I'm not getting any IDE integration. I can't see the types of variables on hover and things like that. After some poking around it turns out I have to go to my VSCode settings and turn on individual Rust Analyzer features. Alpha software is alpha, I guess. I don't know which features I want so I check "Enable All Available Features" and hope nothing bad will happen. My code is immediately enriched with type information and I can see documentation on hover! This is amazing and reminds me of what I loved most about ReasonML, the fact that you don't have to manually declare types but the inferred types are added in by the IDE so you can see them.

I first try

for line in reader.lines() {
    println!("{}", line);
}

but my new IDE integration quickly informs me that line there is a Result type that cannot be printed. Makes sense, I guess reading a line could fail. I try adding one of those convenient question marks:

for line? in reader.lines() {
    println!("{}", line);
}

That's a syntax error. Now that I think about it, the ? operator goes on the operation not the name of the variable being assigned to. How about this?

for line in reader.lines()? {
    println!("{}", line);
}

That's a different error but fortunately Rust error messages are pretty helpful:

the `?` operator can only be applied to values that implement `std::ops::Try`
the trait `std::ops::Try` is not implemented for `std::io::Lines<std::io::BufReader<std::fs::File>>`
required by `std::ops::Try::into_result`

I see what is happening. lines() returns the iterator and we need to unwrap each individual value returned from the iterator, not the iterator itself. I'm starting to think there might not be a nice sugar to do this in a for loop declaration although I'd love if there was, just like Javascript has for await (foo of iter). Unwrapping it where it's used works fine, though, and makes me feel a bit silly for not having started there:

use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;

fn main() -> io::Result<()> {
    let f = File::open("Cargo.toml")?;
    let reader = BufReader::new(f);

    for line in reader.lines() {
        println!("{}", line?);
    }

    Ok(())
}

That compiles and prints out the file! Success!

Before I go for the night, though, I notice that I'm getting an error when trying to auto-format the file. The log says component rustfmt should be in the manifest so I'm guessing rustup doesn't install the formatter by default. Weird choice but OK. I run rustup component add rustfmt but that tells me

toolchain 'stable-x86_64-pc-windows-msvc' does not contain component 'rustfmt' for target 'x86_64-pc-windows-msvc'

Um. Ok. Time to do some Googling. There's an issue where others have had the same problem, the fix seems to be to reinstall the toolchain. I guess my version was too old and rustup got confused? The reinstall fixes it and the autoformatter runs, although my code was already formatted properly so it only reorders the imports. Still, very useful to have in place for later.

Good night everyone!

Posted on by:

Discussion

markdown guide