See a typo? Have a suggestion? Edit this page on Github
Heads up This blog post series has been updated and published as an eBook by FP Complete. I'd recommend reading that version instead of these posts. If you're interested, please check out the Rust Crash Course eBook.
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 Rustrustc
, 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 bycargo
itselfsrc
contains your source files, for now justsrc/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 struct
s and
enum
s 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'sPerson
.- 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 afterFormatter
? 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 anenum
, 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 returnstd::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