Make sure you are using the 2018 edition

New projects created with cargo automatically default to 2018, but for projects that you have started earlier, make sure you have this in your Cargo.toml:

[package]
edition = "2018"

The 2018 edition includes a lot of useful things, but the main two points that are extremely handy, especially for beginners, is Non lexical lifetimes, which makes it easier to please the borrow checker (one of the hardest parts of Rust), and improved match ergonomics that makes match patterns easier to write.

There are no self referential structures

You cannot have structs that refer to themselves, or any kind of loopy structures. What this means is that the following is invalid:

struct Game<'a> {
    fields: [Field<'a>; 3],
    players: [Player; 2]
}

impl<'a> Game<'a> {
    fn mark(&mut self, p: usize, f: usize) {
        self.fields[p] = Field::TAKEN(&self.players[f]);
    }
}

struct Player {
    name: String
}
enum Field<'a> {
    EMPTY,
    TAKEN(&'a Player)
}

fn main() {
    let players = [Player { name: "xxx".to_string() }, Player { name: "yyy".to_string() }];
    let mut game = Game { players, fields: [Field::EMPTY, Field::EMPTY, Field::EMPTY] };
    game.mark(0, 0);
    game.mark(1, 1);
}

In Game::mark, we create a Field::TAKEN variant, and give it the value of &player, which comes from self.players, and then store it in self.fields. This is verboten, you cannot borrow something from self, and then store that in self. What's very annoying, is this is not clearly emphasized by the compiler, you will get an error message like this:

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
 --> oneoff.rs:8:40
  |
8 |         self.fields[p] = Field::TAKEN(&self.players[f]);
  |                                        ^^^^^^^^^^^^^^^
  |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 7:5...
 --> oneoff.rs:7:5
  |
7 | /     fn mark(&mut self, p: usize, f: usize) {
8 | |         self.fields[p] = Field::TAKEN(&self.players[f]);
9 | |     }
  | |_____^
note: ...so that reference does not outlive borrowed content
 --> oneoff.rs:8:40
  |
8 |         self.fields[p] = Field::TAKEN(&self.players[f]);
  |                                        ^^^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 6:6...
 --> oneoff.rs:6:6
  |
6 | impl<'a> Game<'a> {
  |      ^^
note: ...so that the expression is assignable
 --> oneoff.rs:8:26
  |
8 |         self.fields[p] = Field::TAKEN(&self.players[f]);
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  = note: expected  `Field<'a>`
             found  `Field<'_>`

Which doesn't really give you a clue, that the real problem is that you have created a loop. Hopefully the error message will be improved upon.

What you can do in these situations, I'm told, is the following:

  • Store indexes, instead of references
  • Pack the reference in Rc
  • Copy values

There is a trait called Pin, that is supposed to help with loopy structures, but I'm not sure how to use it, so far I haven't found an example (I might be wrong and it's for something else).

Cargo watch

If you have used nodemon, webpack --watch, gulp, this will be familiar. Cargo watch can watch your files and run whatever you want when they change, which can give you a very comfortable feedback cycle while developing, since you can just do:

$ cargo watch -x test

And then every time you make a change, it will rerun your tests.

Tests silence their output by default

On a similar note, if you have println!, dbg!, or anything that writes to stdout in your tests, you will not see the output, unless the test fails. This actually makes sense, since the test runner itself prints to stdout as well, and it will just mix the output of your test, making it hard to read.

So one thing, you can do is to make your test fail purposefully (use panic!()) so you can see your debug output, or you can pass --nocapture to the test runner:

$ cargo test -- --nocapture

Disable unused warnings for tests

I like to develop in a TDD like manner, where I use tests for quick checks, instead of tabbing out and running the program, and one thing I hate (but it does make sense), is that whatever you refer to in your tests, are not counted as "using" those values. So if you are just sketching out your structs/enums, and write some tests, the test runner will complain that all your enum variants/struct fields are unused, and should be removed.

So while locally developing, I disable unused warnings with the following:

$ RUSTFLAGS="-A unused" cargo watch -x "test -- --nocapture"

Edit 2020-04-19

A user on reddit (0xdeadf001) pointed out, that you don't have to use the RUSTFLAGS env var to silence errors during test, you can put #![cfg_attr(test, allow(unused))] on top of your lib.rs/main.rs, and it will silence unused warnings during tests.

Runner

There is a really nice cli tool called Runner that lets you run Rust snippets on the command line, kinda treating Rust as if it was an interpreted language. This is really great if you want to write some quick CLI scripts, that you want to carry around on your different computers, since you don't have to compile them, you can just run it with runner. Eg. you can do this:

$ cat hello.rs
println!("hello {}",args[1]);
$ runner hello.rs dolly
hello dolly

Borrow checker problems? Pack it in Rc, or implement Copy

I know many people will disagree with this, but I spent a lot of time wrestling with the borrow checker, when I tried holding references in my enums/structs. I believe that it's more effective to just pack things in an Rc, so you can keep making progress (not in the sense of performance), than getting stuck with lifetime errors.

Rc gives you selective reference counting, so you can have references without specifying lifetime parameters at all, or you can implement the Copy trait on your structs/enums, so they will be copied, and you no longer have to work with references.

Of course as you are getting familiar with the language, you should drop these, and figure out how to correctly annotate your lifetimes.

The dbg! macro

The dbg! macro is the ghetto debugging of Rust. If you used pprint(), console.log(), die(), println!() for quick one off debugging, this is the same thing, but on steroids. The ergonomics of using dbg! trumps anything else (other than a real debugger), that I have used in other languages.

Here is an example output:

#[derive(Debug)]
struct Player {
    name: String,
    sign: Sign
}

#[derive(Debug)]
enum Sign {
    X, O
}

let player = Player { name: "hello world".to_string(), sign: Sign::X };
dbg!(player);
/**
 * Will print:
 * [/home/you/.cargo/.runner/bin/oneoff.rs:37] player = Player {
 *     name: "hello world",
 *     sign: X,
 * }
 */

Just look at that. It gives you:

  • The line number where the dbg! macro was used
  • The whole expression (so you no longer have to specify what you are logging, eg. no more console.log("cookie", cookie))
  • The debug representation (instead of the stringified representation) of the result

But wait, there is more! The dbg! macro returns the result of the expression, and since if is an expression in Rust as well, you can do this:

let one = "xxx";
let two = "yyy";
if dbg!(one == two) {
    println!("Equals");
}

/**
 * And it will print:
 * [/home/you/.cargo/.runner/bin/oneoff.rs:37] one == two = false
 */

This gives an extremely nice debugging experience, you no longer have to instrument each branch of an if statement to figure out which way the program goes, you can just instrument the if expression itself.

Use clippy

This should be evident by now, since every other article mentions this, but you should regularly run clippy, because not only it gives you lint warnings, but it also gives you tips about how to make your code more idiomatic, or how you can remove redundant things.