Andrew Jakubowicz

@spyr1014

Beginner Bites: A taste of Rust, a safe, concurrent and practical language!

January 23rd 2018

Today we’ll experience a sampler of Rust. I’ll provide links that will get you set up, then you’ll solve the first Project Euler problem in Rust. You’ll see some of the syntax, and learn what a macro is. I hope to show you that the language is robust, easy to use and blindingly fast! It also boasts fearless concurrency which we’ll leave for another post but is a huge selling feature.

Why Rust you may ask? It really depends who you are! University armed me with JavaScript and Python, allowing me to pursue web based projects. Recently, I’ve needed to work closer to the metal to achieve quicker code execution. Rust is quite different to the languages mentioned above as it lacks a garbage collector, and is statically typed. If you run into issues, the compiler error messages are usually great enough to act like an expert pair programmer, otherwise the community is welcoming and helpful.

Check out these companies using Rust in production.

I’ll assume you have some familiarity with executing commands in the terminal or command prompt, and know some programming terminology (like what functions are).

Let’s start with the first Project Euler problem. The problem wants us to find the sum of all the multiples of 3 or 5 below 1000. Before we even tackle the problem we’ll set up and install Rust. We’ll create a project and discuss the difference between a macro and a function. You’ll see how to write unit tests as well as generate pretty documentation. Finally we’ll solve the problem using a loop, and then functionally with an iterator. It’s gonna be a blast 🚀.

I also made this video which you may like!

Installing and Setting up Rust

Many people have worked very hard to make the installation process great, so I’ll point to those resources. If you have Rust installed and you enjoy your setup, please feel free to skip this section. Otherwise, let’s get Rust!

Rustup is a “toolchain installer for the systems programming Rust”. Basically it handles everything for you, and will get you up and running very fast. Go to https://www.rustup.rs/ and follow the instructions for your platform.

Rustup has some nifty features. You can add offline documentation by running the following command rustup component add rust-docs. To access your offline rust documentation type: rustup doc in your terminal or command line. A browser will open with:

  • The Rust Bookshelf
  • API Documentation
  • Extended Error Documentation (for investigating compiler errors)

The Rust Bookshelf is fantastic, and is a great place to go for all levels of Rust users. Online link to these resources here! These resources can be life saving when you’re in the wilderness, writing Rust without wifi 🏕️.

Are we (I)DE yet? lists the code editor support for Rust. As of writing, VSCode and IntelliJ IDEA have the best support. VSCode is a fantastic free option, and is what I use.

Again there are other posts that cover complete setups. I recommend ShaderCat’s post or Asquera’s post. The community is working hard improving the developer experience and these posts may become out of date quite quickly. Searching for the Rust Language Server (RLS) may give more up to date instructions for setting up an IDE.

Of course you can just code in notepad… The compiler is helpful and I respect your decision.

Cargo

“Cargo is the Rust package manager.” ~ The Cargo Book

Cargo is to Rust as NPM is to JavaScript, or pip is to Python, or RubyGems is to Ruby…sort of?

Cargo sets up projects, installs dependencies, builds projects, runs tests, generates documentation and uploads your libraries to crates.io. This is a perfect time to start working on our Project Euler problem. To check that everything installed properly run cargo version in your terminal:

Your version number does not need to match.

Build yourself a new project with the command cargo new euler_sum --bin.

Use `cargo help` to learn more

This command tells Cargo to set up a new application called “euler_sum” in a new folder. By default cargo creates a library, so we’ve used--bin to tell Rust to create an application (“bin” is short for binary). Use cd euler_sum to change your directory into the application folder. This is what you should find:

.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files

The Cargo.toml file is your projects manifest or metadata. If you’re familiar with JavaScript, it’s similar to the package.json file. You list your dependencies here. More information about it can be found in The Cargo Book here, and you can learn about the cargo options with cargo help.

main.rs contains a tiny Rust program:

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

Run the code with cargo run (while in the folder with the Cargo.toml file).

STOP AND CELEBRATE! You’ve run your first Rust program!!!!🎉🎉🎉

fn is the way you declare a function. All application projects require a main function as an entry to your program. This function has no arguments, and doesn’t return anything.

The body of the function contains this word: println!. This is called a macro. It’s looks like a function but it ends with an !. Rust uses macros to do very powerful things, and often libraries use them to be very clever. Let’s quickly learn what makes them different from just a function.

Macro detour

Macros allow you to generate code based on patterns! If you need to copy paste code with minor changes, you could write a macro that writes the code for you. When you compile your project, macros are all expanded (written) first, and then the code is compiled as if you’d written what the macros generate. Basically macros write code for you. Let’s have a look at what the println! macro looks like expanded. Run the following command:

$ rustc src/main.rs --pretty=expanded -Z unstable-options

Here’s my output:

“macros write code for you”

Notice that println! has cleverly generated code to print “Hello, world!” to the terminal. Because macros are able to pattern match, different code is generated based on different inputs! Therefore you can use println! to format strings as well:

println!("Hello, {}!", "lovely humanoid");
// prints -> Hello, lovely humanoid!
println!("Hello, {name}! Want {thing}?", name="Rust", thing="hugs");
// -> Hello, Rust! Want hugs?
println!("{num:>0width$}", num=42, width=4);
// -> 0042

Another benefit of pattern matching is compiler errors if you muck up the macro’s arguments. If you write println!("{}");, the compiler will helpfully say "error: 1 positional argument in format string, but no arguments were given" as well as a cool diagram. You should definitely try it.

Exercises detour 🤔

  • What do the println! macro’s above expand to? The answer may surprise you. More or less code than you expected?
  • Investigate more string formatting options here(Rust by Example) and here(docs).
  • Guess and then find out what the below code outputs:

println!("{0} {1}'s {0} {1}, no matter how small!", "a", "person");

Writing the tests

Because we all want our code test driven as well as type driven, you’re obviously itching to find out how to write tests! All we need here is a simple unit test. If you were writing a library, you could also write doctests.

A test is merely a function that you annotate with an attribute.

#[test]
fn simple_test() {
assert_eq!(solution(10), 23);
}

The #[test] attribute tells Rust that this is a testing function. Therefore this function runs when cargo test is run. assert_eq! panics if the two arguments aren’t equal, thus failing the test. You can also use assert! which takes only a single argument, checking for the argument to evaluate to true.

Run the test using cargo test. (It should error because we haven’t defined the solution function yet.) If you want to see the tests pass, replace the assert_eq!(solution(10), 23); with assert!(true);.

Generating documentation

It’s time to start coding the Project Euler problem. We’ll write the solution in a function called solution.

Let’s start with the type of the solution function:

pub fn solution(max: u64) -> u64 {
unimplemented!
}

This is a public function, that takes an argument max of type u64 and returns a u64. A u64 is a numeric type, called an unsigned integer. These are integers that fit into 64 bits that can only be positive. This immediately tells us that we cannot pass negative numbers into this function, and the function will never return a negative number.

Just for fun lets generate some documentation.

Rust comments can either be the double slash // at the start of the line, or /* multiline comment */. These do not generate documentation. Triple slashes /// before a declaration do generate documentation:

/// `solution` function solves the first Project Euler problem
pub fn solution(max: u64) -> u64 {
// A boring comment
unimplemented!()
}

If you add this code and run the cargo command cargo doc --open, your browser will open with freshly generated documentation! The documentation supports markdown, meaning you can add headings and links. Read more about documentation generation here!

A rough solution to the problem

The below code should look slightly familiar to you. Try to understand what’s going on before reading on. We will be making this much simpler, but this is a great starting point.

pub fn solution(max: u64) -> u64 {
let mut result = 0;
let mut i = 0;
loop {
if i >= max {
break;
}
if i % 3 == 0 || i % 5 == 0 {
result += i;
}
i += 1;
}
return result;
}

Most of this should be fairly self explanatory except maybe the mut. mut is a way of telling Rust when a variable should be mutable. Here we’re saying that result and i need to be mutable. Without mut we would get an error when trying to execute result += i or i += 1. Delete mut and see the error for yourself. In Rust variables are immutable by default, unless explicitly mutable. Rust functions must also explicitly declare if they’re going to be mutating arguments passed in. Function behavior is explicit in the type signature.

There are a lot of things we can clean up here. Firstly imperative code is quite verbose. Instead of using the loop and break, we can use a for loop over a range, like so:

pub fn solution(max: u64) -> u64 {
let mut result = 0;
for i in 0..max {
if i % 3 == 0 || i % 5 == 0 {
result += i;
}
}
return result;
}

Notice we don’t have to manage the variable i anymore. 0..max is the range, and the for loop iterates over the range. A for loop can iterate over any type that implements the Iterator trait. We won’t cover traits here, but for now think of it as an interface (although it’s much more). Because the range implements Iterator, you can use iterator methods like map, filter, and fold.

Filter lets you filter the elements based on a predicate, or a function that returns a boolean. We can thus move the if conditionals into a filter to only get the i values we want:

pub fn solution(max: u64) -> u64 {
let mut result = 0;
for i in (0..max).filter(|n| n % 3 == 0 || n % 5 == 0) {
result += i;
}
return result;
}

What is this funny syntax?

|n| n % 3 == 0 || n % 5 == 0

This is a closure. It’s an anonymous function that can be passed into another function. |n| is the argument/s, and the rest is the function body. The same exact closure can also be written like so (with an explicit function body and return statement):

|n| {
return n % 3 == 0 || n % 5 == 0;
}

In Rust, if your return statement is on the last line of a function, you can omit the return and semicolon to return implicitly. Using that trick we can remove the return from our function:

pub fn solution(max: u64) -> u64 {
let mut result = 0;
for i in (0..max).filter(|n| n % 3 == 0 || n % 5 == 0) {
result += i;
}
// Implicit return below:
result
}

There is still one annoying thing. We have a mutable result variable hanging around. In a larger program this variable could be mutated elsewhere accidentally, and needs to be managed separately. Summing up the values from the iterator doesn’t require another variable and can be done using iterator methods! You could use the fold or sum method.

pub fn solution(max: u64) -> u64 {
(0..max).filter(|n| n % 3 == 0 || n % 5 == 0).sum()
}

Read more about iterators here! There are many iterator methods and they can sometimes be used solely to solve problems. We could also easily introduce fork-join parallelism with a library such as Rayon.

Call your new solution function from the main function and print out your answer. Run the program with cargo run.

🎈Congratulations for completing the first Project Euler question in Rust 🎈. I hope you enjoyed this whirlwind tour! In the future I hope to delve deeper into traits (especially the Iterator trait), macros, concurrency and functional programming. Maybe I’ve convinced you to continue to explore this mind expanding language. Why not continue sampling Rust with Simple Sorting Algorithms in Rust?

If you liked the post, please show it with 👏! I love feedback to please leave a comment or message me on Twitter. I also love ideas, so if there is a blog post you wished existed on Rust, I’d be happy to write it for you!

Follow me on Twitter: @spyr1014

Thank you for reading! ❤

References

More by Andrew Jakubowicz

More Related Stories