How not to learn Rust

16 minute read Published: 2021-12-15

I've seen too many good programmers struggle learning Rust, or even give up.

Here are the mistakes I've seen which may make you fail at learning Rust. I hope this list will help you avoid them.

Mistake 1 : Not be prepared for the first high step

The worst possible way to learn Rust is by vaguely looking at it, or trying some small stuff, in short scattered sessions at night after your demanding work. Sure you did learn a few languages like this before, so you may be confident in your abilities. But at some point in Rust, and it may come soon, you'll encounter a higher step and if you don't fight it with concentration and dedication, you risk not overcome it.

This might be discouraging. Some people give up.

If you go stale and you want to go further, you should

  1. understand that it's normal, because Rust has different concepts that you must learn
  2. stop trying without trying, you're wasting time and motivation
  3. go back to the fight with dedicated time and energy

It should click at some point, and then it's only going better and better, but expect some rough initial times.

Mistake 2 : Dive in without looking at the book

(when we rustaceans say the book, we mean the book but don't let this hide the fact other excellent books have been published since)

Rust is big and not everything is obvious by just messing around. It's important you've at least noticed about traits, macros, smart pointers, etc. and the book offers a short introduction to all major concepts.

So before you dive, have an overview of the book (or books). Maybe don't read it fully, but make sure to have read all titles so that you know when to come back to it later.

Mistake 3 : Start by implementing a graph based algorithm

Old CS courses are full of mathematically pretty algorithms or data structures that are used again and again in whiteboard tests or learning exercises.

The most striking example is the linked list and its variations.

I always see newbies come to Stack Overflow and ask how to write them.

Those structures are hard to do in Rust. And they're very hard to do properly. It's not you: it's the specific model of Rust.

Besides, you don't need a linked list. This is a tool, or an intermediate target if you have the bad design, not a concrete goal. And it's a tool which doesn't fit in this toolbox. It's a little like trying to make your new car run with coal. It's hard and you're trying to use an old solution in the wrong context.

You don't have to be convinced at this point that pointer based lists aren't what you need. Right now, just take it that learning Rust with linked lists is a very bad idea.

A variant of this mistake is to first target a problem which really asks for graphs or references: Make rather this your third or fourth crate. Graphs are often necessary, at least semantically, but learn Rust before, learn about arenas, learn to love the borrow checker and to be familiar with lifetimes.

I'm not making up the linked list point, this was observed first in this famous piece, but linked lists are still one of the favourite reefs for newcomers to sink their ships.

Mistake 4 : Don't read compiler errors

You may be used to compiler errors being at best a vague pointer to the general area where the compiler fails to understand your meaning.

It doesn't work this way in Rust. Rust's compiler is your best teacher who's precisely explaining the problem and suggests a solution.

So, I'd recommend to

The best way to see errors, when beginning at least, is to run cargo check in your console.

related thread on Reddit

Mistake 5 : Ignore compiler warnings

Well, you must ignore them when you're writing code, because there are many of them and most of them are just there because you imported something and you haven't yet written the use, or for other temporary reasons.

But you must look at warnings at two moments:

Most warnings in completed code are in fact errors.

A frequent example is when the compiler tells you that a variable doesn't need to be mut: it's most often because you forgot to mutate it, or you mutated the wrong variable.

Clean those warnings before starting the next task.

And, while you're at it, run cargo clippy from time to time. There will be false positives but there will be some gold too: Clippy often catches logical errors or suggests much cleaner ways to write the same code.

Mistake 6 : Apply best practices from other languages

Most of what made you a good programmer in other languages is still valid when writing Rust, but don't rush when it comes to simple best practices. They may be useless, counter-productive or just not make any sense in Rust.

I list here a few commonly observed variants.

Mistake 6.a: Try to make Rust look OOP

When you don't know where to start, it may be tempting to build a hierarchical class tree. You may want to say that cars are obviously vehicles and that trucks are kind of cars, or the reverse maybe.

Just don't. Don't look for tricks to simulate inheritance. Rust isn't designed this way and it doesn't help to try to make it OOP. It will most notably lead you to ownership problems as OOP designs usually require keeping many references to various parts of the data.

Mistake 6.b: Insist to make it functional

You like pure functions, right ? Side effects are your nemesis so you want to ensure that you only use purely functional constructs.

It turns out that one of the motivational problems behind functional programming is solved another way in Rust.

When we say that Rust's ownership model ensures memory safety, people most often think it means there won't be use before creation or use after free or similar errors. It's actually much more powerful: having only one mutable reference (i.e. exclusive reference) to some data structures means most sources of data inconsistencies are also removed. Without going functional.

So you may use a functional style in Rust, and it's mostly cost-less in the sense that Rust strives to have abstractions paid for at compile time, but you shouldn't force yourself to stick to that style when it doesn't work.

In Rust, when your functional code starts to look hard to decipher and painful to write, ask yourself whether a good old for loop wouldn't be better.

Rust is "multi-paradigm", don't limit it.

Mistake 6.c: Try to make it dynamic

Depending on the languages you did before, you may be tempted to make your functions "generic" by accepting and returning trait objects.

After all, it's a good practice in some other languages to specify the prototype of a function with "interfaces" rather than the specific types, as it allows more freedom.

So you may want to say that your function accepts anything wich implements TraitA and returns something which implements TraitB.

So you could write this:

pub fn fun(my_arg: &dyn TraitA) -> Box<dyn TraitB> {

Something should already bother you there: you have to pass the argument as reference, and you have to return the result in a box. That's because a dyn SomeTrait is unsized: it can't be on the stack on its own.

As you imagine, this has a cost. Which is added to the cost of dynamically looking for the right function to call. And to the cost of missed optimization opportunities.

As you guessed, there's a better solution. Rust has generics and they're powerful.

Most often, the right solution is to ask the compiler to build the necessary implementations for the concrete types which are really in use in the programs. Here, this would be

pub fn fun<A: TraitA>(my_arg: A) -> impl TraitB {

(now, let's admit it: as a new rustician, even returning a impl trait shouldn't really happen)

While there are valid use cases for dyn, most experienced rustacians use it very sparingly (or never). Yes, monomorphization also has a cost but it's not one you should really consider when learning Rust.

Mistake 6.d: Build immutable structures and collections

Bluntly said, you don't need them, thanks again to Rust's ownership model which prevents you from aiming at your foot.

Not giving away a mutable reference solves most problems.

Mistake 6.e: Fine tune privacy and protect your data with getters

As was already said, the ownership model keeps your data protected from concurrent or non desired changes.

The main reason in Rust to make your struct's inner properties private is to ensure consistency.

But most often, Rust reaches this goal by letting your types be essentially consistent, i.e. to ensure that any possible value of the type is consistent, and the magical tool for this is Rust's enum. The most notable exceptions are numerical types that should be in some specific ranges to make sense, and vectors which should always contain a minimal number of values (several solutions exist, none really convenient).

Mistake 6.f: Program defensively

With Option instead of the "billion dollar mistake", with Result instead of exceptions, and with other uses of Rust enums which let you set fields only for the relevant variants, you'll find few cases of data inconsistencies in practice.

So you may relax, trust the input of your fellow functions, and don't lose too much time looking for assertions to add (except in you test units).

You'll probably still have some asserts, for example for vecs that should not be empty, or numbers in a specific range, but they'll be rare compared to other languages, will be mostly dealt with with errors, and shouldn't eat your time and be everywhere in your code.

Mistake 6.g: Use dummy values

There's no need for that.

Instead of returning "", or -1, just return None.

And when computing, to set the value returned by your expression, there's no need for an assignment before it's computed:

Replace

let mut a = 0;
if some_expr() {
	a = some_fun();
} else {
	a = some_other_fun();
}

with

let a = if some_expr() {
	some_fun()
} else {
	some_other_fun()
};

By the way, you'll be happy to learn that using Option<String> or Option<Box<T>> won't take more memory or CPU than the same type without the Option wrapper (More generally, you should let the compiler deal with the trivial optimizations, it's its job, not yours).

Mistake 7 : Build lifetime heavy API

When I designed my first major library, Termimad, I wanted to make it possible to use it with the best possible performances and the least possible memory cost. In the first version of the library (and more specifically of its Minimad dependency that I developed in parallel), there was no String in any of the structs of this text manipulation crate. I was using only references, references everywhere.

The first problem was immediate: writing the library was painful because I had to take lifetimes into account everywhere. You can't just get rid of them.

I discovered the second problem later, and it's deeper: many usages of the API were made more difficult, or less efficient, because of the need to own the data.

When a struct has a reference to data, you must keep the referenced data. And this may be very annoying when it's some dynamic data: you can't just pass the struct and forget the underlying data (well, you can but you don't want to).

There's no general solution for this problem. I made alternate structures since to help deal with it in Termimad but I still regret not having initially made the simple choice of having my library structs own the data.

Not everybody will make this mistake, but those who do may pay it for a long time.

A short initial version of how to avoid this mistake for new Rust developers would be: When designing the structs of a new library, prefer to own the data. Don't prematurely optimize with reference based structs.

Because it's easy (and not so expensive) to clone data to pass them to an API, but it's sometimes very painful to keep data so that you can pass references around.

Mistake 8 : Ignore non standard libraries

Many tools that you may think are basic aren't in the standard library.

Overall, this is a choice I really like, because it made it possible to have the best solution emerge, but it makes it harder for new Rust programmers to find the library they need.

If something doesn't seem to be in the standard libraries (and sometimes even when it is), look for the "non standard" libraries.

For example, those crates are kind of standard, high quality libraries which are used by many programs, and you can't ignore them just like you can't ignore std:

Of course they may be replaced by better ones in the future but right now you wouldn't take a risk by using them.

There are many problems for which several solutions exist so the choice won't be obvious but it's easy enough to find them and compare them. Ask Google and search crates.io or lib.rs, read the READMEs (ignore the "blazingly fast" assertions) and check the numbers of stars and the dependents, ask around.

Mistake 9 : Use unsafe, abuse unwrap

Yes unsafe is useful, and yes it's made to be used.

But you probably don't need it when learning Rust.

If you're building something normal, and you fall on a function in the documentation asking for unsafe, look again: there's probably a safe solution nearby. And unsafe isn't the magical solution to cut corners and make the code faster.

As for unwrap, it's a little too present in the literature exposed to new programmers, because it's there in most Stack Overflow answers and in most documentation snippets.

So many new rustaceans think it's normal to unwrap all the things.

It's not, unless you're writing a small snippet yourself.

The main problem when using unwrap too lightly is that you may fail to notice that the None or Error isn't so exceptional. Handling the error case forces you to think about that alternate branch, see why it can, in fact, happen, and deal with it. In some cases you'll escalate the problem to the calling code which may have more context, it's better than unwrap. And in the few cases for which you can prove it's safe to unwrap, you'd better add a comment explaining why.

Even when starting a new small program, it's as easy to properly deal with errors, while unwrap will not only make your program buggy, it will also make it harder to analyze and relying on unwrap will make you miss the logically sound design.

A popular intermediate position, when prototyping or writing a short snippet, is to use expect which helps state why you think the case shouldn't happen.

Pick one of the popular error managing libraries, properly declare your functions with Result when it's needed, and your life will be easier. And you won't miss the big satisfaction of having a program running right at the first try, which might be the most defining experience of Rust.

Mistake 10 : Don't look at the sources

Rust documentation is usually said to be excellent.

So you don't need me to tell you to look at it, to read the snippets or to follow links from type to type.

But you may ignore the small "[src]" links near the texts. This would be an error.

Follow the link from the documentation to the code and not only will you better understand the function you're looking at, but you'll also have Rust demystified (most code is very simple) and you'll be exposed to what's usually called "idiomatic" Rust code, which really means it best uses the features of the language.

The more you'll read beautiful source, the sooner you'll write beautiful code.

Mistake 11 : Design everything from the start

Two good reasons to not overly design for the unclear future:

Build your small brick, make it work, test it with Rust's very convenient unit tests. If the shape isn't perfect for the wall you're building, no problem, you'll change that shape later. And if the design pattern you wanted to follow appears unpractical or heavy either for Rust or for your application, then you'll change it too.

So build your thing brick by brick and refine the global plan in parallel, it doesn't need to be complete or stay unchanged.

Mistake 12: Learn the wrong Rust

This last chapter comes back to some of the previous ones and propose a list of red flags.

If you use any of the following words in your first programs, you might be learning Rust the wrong way, or worse, you might be learning the wrong Rust, the one which will make you suffer and build poor crates.