I also blog frequently on the Yesod Web Framework blog, as well as the FP Complete blog.

Kick the Tires - Rust Crash Course lesson 1

See a typo? Have a suggestion? Edit this page on Github

In this lesson, we just want to get set up with the basics: tooling, ability to compile, basic syntax, etc. Let’s start off with the tooling, you can keep reading while things download.

This post is part of a series based on teaching Rust at FP Complete. If you’re reading this post outside of the blog, you can find links to all posts in the series at the top of the introduction post. You can also subscribe to the RSS feed.

Tooling

Your gateway drug to Rust will be the rustup tool, which will install and manage your Rust toolchains. I put that in the plural, because it can manage both multiple versions of the Rust compiler, as well as cross compilers for alternative targets. For now, we’ll be doing simple stuff.

Both of these pages will tell you to do the same thing:

  • On Unix-like systems, run curl https://sh.rustup.rs -sSf | sh
  • Or run a Windows installer, probably the 64-bit installer

Read the instructions on the rust-lang page about setting up your PATH environment variable. For Unix-like systems, you’ll need ~/.cargo/bin in your PATH.

Along with the rustup executable, you’ll also get:

  • cargo, the build tool for Rust
  • rustc, the Rust compiler

Hello, world!

Alright, this part’s easy: cargo new hello && cd hello && cargo run.

We’re not learning all about Cargo right now, but to give you the basics:

  • Cargo.toml contains the metadata on your project, including dependencies. We won’t be using dependencies quite yet, so the defaults will be fine.
  • Cargo.lock is generated by cargo itself
  • src contains your source files, for now just src/main.rs
  • target contains generated files

We’ll get to the source code itself in a bit, first a few more tooling comments.

Building with rustc

For something this simple, you don’t need cargo to do the building. Instead, you can just use: rustc src/main.rs && ./main. If you feel like experimenting with code this way, go for it. But typically, it’s a better idea to create a scratch project with cargo new and experiment in there. Entirely your decision.

Running tests

We won’t be adding any tests to our code yet, but you can run tests in your code with cargo test.

Extra tools

Two useful utilities are the rustfmt tool (for automatically formatting your code) and clippy (for getting code advice). Note that clippy is still a work in progress, and sometimes gives false positives. To get them set up, run:

$ rustup component add clippy-preview rustfmt-preview

And then you can run them with:

$ cargo fmt
$ cargo clippy

IDE

There is some IDE support for those who want it. I’ve heard great things about IntelliJ IDEA’s Rust add-on. Personally, I haven’t used it much yet, but I’m also not much of an IDE user in the first place. This crash course won’t assume any IDE, just basic text editor support.

Macros

Alright, we can finally look out our source code in src/main.rs:

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

Simple enough. fn says we’re writing a function. The name is main. It takes no arguments, and has no return value. (Or, more accurately, it returns the unit type, which is kind of like void in C/C++, but really closer to the unit type in Haskell.) String literals look pretty normal, and function calls look almost identical to other C-style languages.

Alright, here’s the first “crash course” part of this: why is there an exclamation point after the println? I say “crash course” because when I first learned Rust, I didn’t see an explanation of this, and it bothered me for a while.

println is not a function. It’s a macro. This is because it takes a format string, which needs to be checked at compile time. To prove the point, try changing the string literal to include {}. You’ll get an error message along the lines of:

error: 1 positional argument in format string, but no arguments were given

This can be fixed by providing an argument to fill into the placeholder:

println!("Hello , world! {} {} {}", 5, true, "foobar");

Take a guess at what the output will be, and you’ll probably be right. But that leaves us with a question: how does the println! macro know how to display these different types?

Traits and Display

More crash course time! To get a better idea of how displaying works, let’s trigger a compile time error. To do this, we’re going to define a new data type called Person, create a value of that type, and try to print it:

struct Person {
    name: String,
    age: u32,
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("Person: {}", alice);
}

We’ll get into more examples on defining your own structs and enums later, but you can cheat and read the Rust book if you’re curious.

If you try to compile that, you’ll get:

error[E0277]: `Person` doesn't implement `std::fmt::Display`
  --> src/main.rs:11:28
   |
11 |     println!("Person: {}", alice);
   |                            ^^^^^ `Person` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Person`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: required by `std::fmt::Display::fmt`

That’s a bit verbose, but the important bit is the trait `std::fmt::Display` is not implemented for `Person`. In Rust, a trait is similar to an interface in Java, or even better like a typeclass in Haskell. (Noticing a pattern of things being similar to Haskell concepts? Yeah, I did too.)

We’ll get to all of the fun of defining our own traits, and learning about implementing them later. But we’re crashing forward right now. So let’s throw in an implementation of the trait right here:

impl Display for Person {
}

That didn’t work:

error[E0405]: cannot find trait `Display` in this scope
 --> src/main.rs:6:6
  |
6 | impl Display for Person {
  |      ^^^^^^^ not found in this scope
help: possible candidates are found in other modules, you can import them into scope
  |
1 | use core::fmt::Display;
  |
1 | use std::fmt::Display;
  |

error: aborting due to previous error

For more information about this error, try `rustc --explain E0405`.
error: Could not compile `foo`.

We haven’t imported Display into the local namespace. The compiler helpfully recommends two different traits that we may want, and tells us that we can use the use statement to import them into the local namespace. We saw in an earlier error message that we wanted std::fmt::Display, so adding use std::fmt::Display; to the top of src/main.rs will fix this error message. But just to prove the point, no use statement is necessary! We can instead us:

impl std::fmt::Display for Person {
}

Awesome, our previous error message has been replaced with something else:

error[E0046]: not all trait items implemented, missing: `fmt`
 --> src/main.rs:6:1
  |
6 | impl std::fmt::Display for Person {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `fmt` in implementation
  |
  = note: `fmt` from trait: `fn(&Self, &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error>`

We’re quickly approaching the limit of things we’re going to cover in a “kicking the tires” lesson. But hopefully this will help us plant some seeds for next time.

The error message is telling us that we need to include a fmt method in our implementation of the Display trait. It’s also telling us what the type signature of this is going to be. Let’s look at that signature, or at least what the error message says:

fn(&Self, &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error>

There’s a lot to unpack there. I’m going to apply terminology to each bit, but you shouldn’t expect to fully grok this yet.

  • Self is the type of the thing getting the implementation. In this case, that’s Person.
  • Adding the & at the beginning makes it a reference to the value, not the value itself. C++ developers are used to that concept already. Many other languages talk about pass by reference too. In Rust, this plays in quite a bit with ownership. Ownership is a massively important topic in Rust, and we’re not going to discuss it more now.
  • &mut is a mutable reference. By default, everything in Rust is immutable, and you have to explicitly say that things are mutable. We’ll later get into why mutability of references is important to ownership in Rust.
  • Anyway, the second argument is a mutable reference to a Formatter. What’s the <'_> thing after Formatter? That’s a lifetime parameter. That also has to do with ownership. We’ll get to lifetimes later as well.
  • The -> indicates that we’re providing the return type of the function.
  • Result is an enum, which is a sum type, or a tagged union. It’s generic on two type parameters: the value in case of success and the value in case of error.
  • In the case of success, our function returns a (), or unit value. This is another way of saying “I don’t return any useful value if things go well.” In the case of an error, we return std::fmt::Error.
  • Rust has no runtime exceptions. Instead, when something goes wrong, you return it explicitly. Almost all code uses the Result type to track things going wrong. This is more explicit than exception-based languages. But unlike languages like C, where it’s easy to forget to check the type of a return to see if it succeeded, or tedious to do error handling properly, Rust makes this much less painful. We’ll deal with it later.
    • NOTE Rust does have the concept of panics, which in practice behave similarly to runtime exceptions. However, there are two important differences. Firstly, by convention, code is not supposed to use the panic mechanism for signaling normal error conditions (like file not found), and instead reserve panics for completely unexpected failures (like logic errors). Secondly, panics are (mostly) unrecoverable, meaning they take down the current thread.

      A previous version of this document said that panics are unrecoverable, and that they take down the entire thread. However, as pointed out by J Haigh, this isn’t quite true: the function catch_unwind allows you to usually capture and recover from a panic without losing the current thread. I’m not going to go into more details here.

Awesome, that type signature all on its own gave us enough material for about 5 more lessons! Don’t worry, you’ll be able to write some Rust code without understanding all of those details, as we’ll demonstrate in the rest of this lesson. But if you’re really adventurous, feel free to explore the Rust book for more information.

Semicolons

Let’s get back to our code, and actually implement our fmt method:

struct Person {
    name: String,
    age: u32,
}

impl std::fmt::Display for Person {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        write!(fmt, "{} ({} years old)", self.name, self.age)
    }
}

fn main() {
    let alice = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("Person: {}", alice);
}

We’re using the write! macro now, to write content into the Formatter provided to our method. This is beyond the scope of our discussion, but this allows for more efficient construction of values and production of I/O than producing a bunch of intermediate strings. Yay efficiency.

The &self parameter of the method is a special way of saying “this is a method that works on this object.” This is quite similar to how you’d write code in Python, though in Rust you have to deal with pass by value vs pass by reference.

The second parameter is named fmt, and &mut Formatter is its type.

The very observant among you may have noticed that, above, the error message mentioned &Self. In our implementation, however, we made a lower &self. The difference is that &Self refers to the type of the value, and the lower case &self is the value itself. In fact, the &self parameter syntax is basically sugar for self: &Self.

Does anyone notice something missing? You may think I made a typo. Where’s the semicolon at the end of the write! call? Well, first of all, copy that code in and run it to prove to yourself that it’s not a typo, and that code works. Now add the semicolon and try compiling again. You’ll get something like:

error[E0308]: mismatched types
 --> src/main.rs:7:81
  |
7 |       fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
  |  _________________________________________________________________________________^
8 | |         write!(fmt, "{} ({} years old)", self.name, self.age);
  | |                                                              - help: consider removing this semicolon
9 | |     }
  | |_____^ expected enum `std::result::Result`, found ()
  |
  = note: expected type `std::result::Result<(), std::fmt::Error>`
             found type `()`

This is potentially a huge confusion in Rust. Let me point out something else that you may have noticed, especially if you come from a C/C++/Java background: we have a return value from our method, but we never used return!

The answer to that second concern is easy: the last value generated in a function in Rust is taken as its return value. This is similar to Ruby and—yet again—Haskell. return is only needed for early termination.

But we’re still left with our first question: why don’t we need a semicolon here, and why does adding the semicolon break our code? Semicolons in Rust are used for terminating statements. A statement is something like the use statement we saw before, or the let statement we briefly demonstrated here. The value of a statement is always unit, or (). That’s why, when we add the semicolon, the error message says found type `()`. Leaving off the semicolon, the expression itself is the return value, which is what we want.

You’ll see the phrase that Rust is an expression-oriented language, and this kind of thing is what it’s referring to. You can see mention of this in the FAQ. Personally, I find that the usage of semicolon like this can be subtle, and I still instinctively trip up on it occasionally when my C/C++/Java habits kick in. But fortunately the compiler helps identify these pretty quickly.

Numeric types

Last concept before we just start dropping in some code. We’re going to start off by playing with numeric values. There’s a really good reason for this in Rust: they are copy values, values which the compiler automatically clones for us. Keep in mind that a big part of Rust is ownership, and tracking who owns what is non-trivial. However, with the primitive numeric types, making copies of the values is so cheap, the compiler will do it for you automatically. This is some of that automatic magic I mentioned in my intro post.

To demonstrate, let’s check out some code that works fine with numeric types:

fn main() {
    let val: i32 = 42;
    printer(val);
    printer(val);
}

fn printer(val: i32) {
    println!("The value is: {}", val);
}

We’ve used a let statement to create a new variable, val. We’ve explicitly stated that its type is i32, or a 32-bit signed integer. Typically, these kinds of type annotations are not needed in Rust, as it will usually be able to infer types. Try leaving off the type annotation here. Anyway, we then call the function printer on val twice. All good.

Now, let’s use a String instead. A String is a heap-allocated value which can be created from a string literal with String::from. (Much more on the many string types later). It’s expensive to copy a String, so the compiler won’t do it for us automatically. Therefore, this code won’t compile:

fn main() {
    let val: String = String::from("Hello, World!");
    printer(val);
    printer(val);
}

fn printer(val: String) {
    println!("The value is: {}", val);
}

You’ll get this intimidating error message:

error[E0382]: use of moved value: `val`
 --> src/main.rs:4:13
  |
3 |     printer(val);
  |             --- value moved here
4 |     printer(val);
  |             ^^^ value used here after move
  |
  = note: move occurs because `val` has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

Exercise 1 there are two easy ways to fix this error message: one using the clone() method of String, and one that changes printer to take a reference to a String. Implement both solutions. (Solutions will be posted separately in a few days.)

Printing numbers

We’re going to tie off this lesson with a demonstration of three different ways of looping to print the numbers 1 to 10. I’ll let readers guess which is the most idiomatic approach.

loop

loop creates an infinite loop.

fn main() {
    let i = 1;

    loop {
        println!("i == {}", i);
        if i >= 10 {
            break;
        } else {
            i += 1;
        }
    }
}

Exercise 2 This code doesn’t quite work. Try to figure out why without asking the compiler. If you can’t find the problem, try to compile it. Then fix the code.

If you’re wondering: you could equivalently use return or return () to exit the loop, since the end of the loop is also the end of the function.

while

This is similar to C-style while loops: it takes a condition to check.

fn main() {
    let i = 1;

    while i <= 10 {
        println!("i == {}", i);
        i += 1;
    }
}

This has the same bug as the previous example.

for loops

For loops let you perform some action for each value in a collection. The collections are generated lazily using iterators, a great concept built right into the language in Rust. Iterators are somewhat similar to generators in Python.

fn main() {
    for i in 1..11 {
        println!("i == {}", i);
    }
}

Exercise 3: Extra semicolons

Can you leave out any semicolons in the examples above? Instead of just slamming code into the compiler, try to think through when you can and cannot drop the semicolons.

Exercise 4: FizzBuzz

Implement fizzbuzz in Rust. The rules are:

  • Print the numbers 1 to 100
  • If the number is a multiple of 3, output fizz instead of the number
  • If the number is a multiple of 5, output buzz instead of the number
  • If the number is a multiple of 3 and 5, output fizzbuzz instead of the number

Next time

Next time, the plan is to get into more details on ownership, though plans are quite malleable in this series. Stay tuned!

Rust at FP Complete | Introduction

Blog archive