Mastering Rust

4.6 (9 reviews total)
By Vesa Kaihlavirta
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Getting Your Feet Wet

About this book

If concurrent programs are giving you sleepless nights, Rust is your go-to language. Being one of the first ever comprehensive books on Rust, it is filled with real-world examples and explanations, showing you how you can build scalable and reliable programs for your organization.

We’ll teach you intermediate to advanced level concepts that make Rust a great language. Improving performance, using generics, building macros, and working with threads are just some of the topics we’ll cover. We’ll talk about the official toolsets and ways to discover more. The book contains a mix of theory interspersed with hands-on tasks, so you acquire the skills as well as the knowledge. Since programming cannot be learned by just reading, we provide exercises (and solutions) to hammer the concepts in.

After reading this book, you will be able to implement Rust for your enterprise project, deploy the software, and will know the best practices of coding in Rust.

Publication date:
May 2017
Publisher
Packt
Pages
354
ISBN
9781785885303

 

Chapter 1. Getting Your Feet Wet

Since you're already an accomplished programmer, this chapter will go through the design philosophy of Rust and the basics of the language in a rather speedy fashion. Each subsection will contain example code and runs of the compiler, the output given by it (if any), and there will be more than a dozen code examples.

Programming is a unique combination of knowledge and craft, both being equally important. To get on the path of mastering a craft, you need practice, which is why I recommend that you write, not copy/paste, every piece of code you see here manually.

Here are the topics covered in this chapter:

  • Installing the Rust compiler and the Cargo build tool
  • Language features: variables, conditionals, loops, primitive types, compound types, and sequences
  • A final exercise for honing your skills with the compiler
 

What is Rust and why should you care?


Rust is a programming language originally started by Graydon Hoare in ­2006. It's currently an open source project, developed mainly by a team in Mozilla and other developers. The first stable version, 1.0, was released in 2015.

While being a general purpose language, it is aiming for the space where C and C++ have dominated. Its defining principles that underline many of its design decisions are zero-cost abstractions and compiler-assisted resource safety.

One example of zero-cost abstractions is seen in Rust's iterators. They are an abstraction over loops that go through sequences, in roughly the same level that a markedly higher-level language such as Ruby has. However, their runtime cost is zero; they compile down to the same (or better) assembler code as you would have gotten by writing the same loop by hand.

Resource safety means that in Rust code and your resources (memory, file handles, and database references) can be analyzed by the compiler as safe to use. A most typical error in a C program is the memory-access error, where memory is used after being freed or is forgotten to be freed. In other languages, you might be spared from memory bugs by automatic garbage collection, but that may or may not help you with other types of resources such as file pointers. It gets even worse if you introduce concurrency and shared memory.

Rust has a system of borrows and lifetimes; plus, it replaces the concept of a null pointer with error types. These decisions raise the complexity of the language by a fair bit but make many errors impossible to make.

Last but not least, Rust's community is quite unusually active and friendly. Stack Overflow's Developer Survey in 2016 selected it as the most-loved programming language, so it can be said that the overall programming community is very interested in it.

To summarize, you should care about Rust because you can write high performing software with less bugs in it while enjoying many modern language features and an awesome community!

 

Installing Rust compiler and Cargo


The Rust toolset has two major components: the compiler (rustc) and a combined build tool or dependency manager (Cargo). This toolset comes in three frequently released versions:

  • Nightly: This is the daily successful build of the master development branch. This contains all the features, some of which are unstable.
  • Beta: This is released every six weeks; a new beta branch is taken from nightly. It contains only features that are flagged as stable.
  • Stable: This is released every six weeks; the previous beta branch becomes the new stable.

Developers are encouraged to mainly use stable. However, the nightly version enables many useful features, which is why some libraries and programs require it.

Using rustup.rs

To make it easier for people in various platforms to download and install the standard tools, the Rust team developed rustup. The rustup tool provides a way to install prebuilt binaries of the Rust toolset (rustc and Cargo) easily for your local user. It also allows installing various other components, such as Rust source code and documentation.

The officially supported way to install Rust is to use rustup.rs:

curl https://sh.rustup.rs -sSf | sh 

This command will download the installer and run it. The installer will, by default, install the stable version of the Rust compiler, the Cargo build tool, and the API documentation. They are installed by default for the current user under the .cargo directory, and rustup will also update your PATH environment variable to point there.

Here's how running the command should look:

If you need to make any changes to your installation, choose 2. But these defaults are fine for us, so we'll go ahead and choose 1. This is what the output should look like afterwards:

Now, you should have everything you need to compile and run programs written in Rust. Let's try it!

 

A tour of the language and trying it out


For the fundamental language features, Rust does not stray far from what you are used to. Programs are defined in modules; they contain functions, variables, and compound data structures. Here's how a minimal program looks:

fn main() { 
  println!("Are you writing this or reading it?"); 
}  

Try compiling and running this. Write it to a file called main.rs and then run the Rust compiler:

> rustc -o main main.rs 
> ./main 
Are you writing this or reading it? 

Running rustc manually is not how you will do it for real programs, but it will do for these small programs. A fine alternative to running small pieces of code is to use the Rust Playground service in http://play.rust-lang.org:

The program itself is fairly simple: the fn keyword is used to define functions, followed by the function name, its arguments inside parentheses, and the function body inside curly braces. Nothing new (except some syntax) there. The exclamation mark after the print-line call means that it's actually not a function, but a macro. This just means that it performs some expansions at compile time rather than doing all the work at runtime. If you are familiar with macros from other languages such as C or LISP, Rust macros will be familiar as well. Macros will be covered more in Chapter 9, Compiler Plugins.

Variables are defined with the let keyword. Rust has a local type inference, which means that the types of function variables are figured out by the compiler, and the coder can almost always omit them. It can easily lead to improved readability of the source code, especially in the case of frequently used static strings:

// first-program.rs 
fn main() { 
    let target_inferred = "inferred world";           
    // these two variables  
    let target: &'static str = "non-inferred world"; // have identical types  
    println!("Hi there, {}", target_inferred);  
    println!("Hi there, {}", target);  
}  

The strings in this program are string literals or, more specifically, string slices with a static lifetime. Strings will be covered in Chapter 4, Types, and lifetimes in Chapter 6, Memory, Lifetimes, and Borrowing.

Comments in code are written like in C, // for single line comments, and /* */ blocks for multiline comments.

Constants and variables

Rust deviates from the mainstream here by making constants the default variable type. If you need a variable that can be mutated, you use the let mut keyword:

// variables.rs 
fn main() { 
  let mut target = "world"; 
  println!("Howdy, {}", target); 
  target = "mate"; 
  println!("Howdy, {}", target); 
} 

Conditionals should also look familiar; they follow the C-like if...else pattern. Since Rust is strongly-typed, the condition must be a Boolean type:

// conditionals.rs 
fn main() { 
  let condition = true; 
  if condition { 
    println!("Condition was true"); 
  } else { 
    println!("Condition was false"); 
  } 
} 

In Rust, if is not a statement but an expression. This distinction means that if always returns a value. The value may be an empty type that you don't have to use, or it may be an actual value. This means that you can use the if expression as tertiary expressions are used in some languages:

// if-expression.rs 
fn main() { 
  let result = if 1 == 2 { 
    "Nothing makes sense" 
  } else { 
    "Sanity reigns" 
  }; 

  println!("Result of computation: {}", result); 
} 

Take a closer look at the preceding program; it highlights an important detail regarding the semicolon and blocks. The semicolon is not optional in Rust, but it has a specific meaning. The last expression of a block is the one whose value is returned out of a block, and the absence of the semicolon in the last line is important; if we were to add a semicolon after the strings in the if blocks, Rust would interpret it as you wanting to throw the value away:

// semicolon.rs 
fn main() { 
  let result = if 1 == 2 { 
    "Nothing makes sense"; 
  } else { 
    "Sanity reigns"; 
  };

  println!("Result of computation: {:?}", result); 
} 

In this case, the result will be empty, which is why we had to change the println! expression slightly; this type cannot be printed out in the regular way. More about that in Chapter 4, Types, where we talk about types.

Loops

Simple loops are programmed with either the while loop (if a condition for the looping is wanted) or with loop (if no condition is wanted). The break keyword gets you out of the loop. Here's an example of using the loop keyword:

// loop.rs 
fn main() { 
  let mut x = 1000; 
  loop { 
        if x < 0 { 
                 break; 
            } 
            println!("{} more runs to go", x); 
            x -= 1; 
    } 
} 

An example of while loop is as follows:

// while.rs 
fn main() { 
  let mut x = 1000; 
  while x > 0 { 
            println!("{} more runs to go", x); 
            x -= 1;     
  } 
} 

Compound data

For defining custom data types, there are structs. The simpler form is called a tuple struct, where the individual fields are not named but are referred to by their position. This should mostly be used when your data consists of only one or a few fields to achieve better levels of type safety, such as here:

// tuplestruct.rs 
#[derive(PartialEq)]
struct Fahrenheit(i64);

#[derive(PartialEq)]
struct Celsius(i64);

fn main() {
 let temperature1 = Fahrenheit(10);
 let temperature2 = Celsius(10);

 println!("Is temperature 1 the same as temperature 2? Answer: {}", 
           temperature1 == temperature2);

 println!("Temperature 1 is {} fahrenheit", temperature1.0);
 println!("Temperature 2 is {} celsius", temperature2.0);
}

What is inside the tuple struct can be accessed by the .<number> operation, where the number refers to the position of the field in the struct.

This is the first piece of code in this book that fails to compile, and the reason is that while the two temperatures get the equals methods derived for them, they will only be defined for comparing the same types. Since comparing Fahrenheit with Celsius without any sort of conversion does not make sense, you can fix this piece of code by either removing the last println! invocation or by comparing temperature1 against itself. The derive line before the structs generated code that allows == operation to work against the same type.

Here's how the compiler tells you this:

The other form of structs has named fields:

// struct.rs 
struct Character { 
  strength: u8, 
  dexterity: u8, 
  constitution: u8, 
  wisdom: u8, 
  intelligence: u8, 
  charisma: u8, 
  name: String 
} 

fn main() { 
   let char = Character { strength: 9, dexterity: 9, constitution: 9, 
   wisdom: 9, intelligence: 9, charisma: 9, 
   name: "Generic AD&D Hero".to_string() }; 

   println!("Character's name is {}, and his/her strength is {}", char.name, char.strength); 
}  

In the preceding struct, you can see the usage of a primitive type, the unsigned 8-bit integer (u8). Primitive types by convention start with a lowercase character, whereas other types start with a capital letter (such as String up there). For reference, here's a full table of all primitive types:

Type

Description

Possible values

bool

Booleans

true, false

u8/u16/u32/u64

Fixed size unsigned integers

Unsigned range determined by bit size

i8/i16/i32/i64

Fixed size signed integers

Signed range determined by bit size

f32/f64

Fixed size floats

Float range determined by bit size (IEEE-754)

usize

Architecture-dependant unsigned integer

Depending on target machine, usually 32 or 64 bit value

isize

Architecture-dependant signed integer

Depending on target machine, usually 32 or 64 bit value

char

Single unicode character

4 bytes describing a unicode character

str

String slice

Unicode string

[T; N]

Fixed-size arrays

N number of type T values

&[T]

Slices

References to values of type T

(T1, T2, ...)

Tuples

Elements of types T1, T2, ...

fn(T1, T2, ...) → R

Functions

Functions that take types T1, T2, ... as parameters, returns value of type R

Enums and pattern matching

Whenever you need to model something that can be of several different types, enums may be a good choice. The enum variants in Rust can be defined with or without data inside them, and the data fields can be either named or anonymous:

enum Direction { 
  N, 
  NE, 
  E, 
  SE, 
  S, 
  SW, 
  W, 
  NW 
} 

enum PlayerAction { 
  Move(direction: Direction, speed: u8), 
  Wait, 
  Attack(Direction)   
} 

This defines two enum types: Direction and PlayerAction. For each of these enum types, this also defines a number of namespaced enum variants: Direction::N, Direction::NE, and so on for the Direction type, and PlayerAction::Move, PlayerAction::Wait, and PlayerAction::Attack for the PlayerAction type.

The most typical way of working with enums is pattern matching with the match expression:

#[derive(Debug)]
enum Direction {
  N,
  NE,
  E,
  SE,
  S,
  SW,
  W,
  NW,
}

enum PlayerAction {
  Move {
    direction: Direction,
    speed: u8,
  },
  Wait,
  Attack(Direction),
}

fn main() {
  let simulated_player_action = PlayerAction::Move {
    direction: Direction::NE,
    speed: 2,
  };

  match simulated_player_action {
    PlayerAction::Wait => println!("Player wants to wait"),
    PlayerAction::Move { direction, speed } => {
      println!("Player wants to move in direction {:?} with speed {}",
                direction, speed)
    }
    PlayerAction::Attack(direction) => {
      println!("Player wants to attack direction {:?}", direction)
    }
  };
} 

Like if, match is also an expression, which means that it returns a value, and that value has to be of the same type in every branch. In the preceding example, it's what println!() returns, that is, the empty type.

The derive line above the first enum tells the compiler to generate code for a Debug trait. Traits will be covered more in Chapter 4, Types, but for now, we can just note that it makes the println! macro's {:?} syntax work properly. The compiler tells us if the Debug trait is missing and gives suggestions about how to fix it:

 

Struct methods


It's often the case that you wish to write functions that operate on a specific struct or return the values of a specific struct. That's when you write implementation blocks with the impl keyword.

For instance, we could extend the previously defined character struct with two methods: a constructor that takes a name and sets default values for all the character attributes and a getter method for character strength:

// structmethods.rs 
struct Character { 
  strength: u8, 
  dexterity: u8, 
  constitution: u8, 
  wisdom: u8, 
  intelligence: u8, 
  charisma: u8, 
  name: String, 
} 

impl Character { 
  fn new_named(name: String) -> Character { 
    Character {  
      strength: 9, 
      constitution: 9, 
      dexterity: 9,
      wisdom: 9, 
      intelligence: 9, 
      charisma: 9, 
      name: name, 
    } 
  } 

  fn get_strength(&self) -> u8 { 
    self.strength 
  } 
} 

The new_named method is called an associated function because it does not take self as the first parameter. It is not far from what many other languages would call a static method. It is also a constructor method since it follows the convention of starting with the word, new, and because it returns a struct of the same type (Character) for which we're defining an implementation. Since new_named is an associated function, it can be called by prefixing the struct name and double colon:

Character::new_named("Dave")  

The self parameter in get_strength is special in that its type is inferred to be the same as the impl block's type, and because it is the thing that makes get_strength a callable method on the struct. In other words, get_strength can be called on an already created instance of the struct:

let character = Character::new_named("Dave"); 
character.get_strength(); 

The ampersand before self means that self is borrowed for the duration of the method, which is exactly what we want here. Without the ampersand, the ownership would be moved to the method, which means that the value would be deallocated after leaving get_strength. Ownerships are a distinguishing feature of Rust, and will be dealt in depth in Chapter 6, Memory, Lifetimes, and Borrowing.

Using other pieces of code in your module

A quick word about how to include code from other places into the module you are writing. Rust's module system has its own pecularities, but it's enough to note now that the use statement brings code from another module into the current namespace. It does not load external pieces of code, it merely changes the visibility of things:

// use.rs 
use std::ascii::AsciiExt; 

fn main() { 
  let lower_case_a = 'a'; 
  let upper_case_a = lower_case_a.to_ascii_uppercase(); 

  println!("{} upper cased is {}", lower_case_a, upper_case_a); 
} 

In this example, the AsciiExt module contains an implementation of the to_ascii_uppercase for the char type, so including that in this module makes it possible to use the method here. The compiler manages again to be quite helpful if you miss a particular use statement, like what happens here if we remove the first line and try to compile:

Sequences

One more thing to cover and then we can wrap up the basics. Rust has a few built-in ways to construct sequences of data: arrays and tuples. Then, it has a way to take a view to a piece of that data: slices. Thirdly, it has several data structures as libraries, of which we will cover Vectors (for dynamically growable sequences) and HashMaps (for key/value data).

Arrays are C-like: they have a fixed length that you need to specify along with the type of the elements of the array when declaring it. The notation for array types is [<type>, <size>]:

// arrays.rs 
fn main() { 
  let numbers: [u8; 10] = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11]; 
  let floats = [0.1, 0.2, 0.3]; 

  println!("The first number is {}", numbers[0]); 

  for number in &numbers { 
    println!("Number is {}", number); 
  } 

  for float_number in &floats { 
    println!("Float is {}", float_number); 
  } 
} 

As said before, Rust is able to infer the types of local variables, so writing them out is optional.

Slices offer a way to safely point to a continuous range in an existing data structure. The type of slices is &[T]. Its syntax looks similar to arrays:

// slices.rs 
fn main() { 
  let numbers: [u8; 4] = [1, 2, 4, 5]; 
  let all_numbers_slice: &[u8] = &numbers[..]; 
  let first_two_numbers: &[u8] = &numbers[0..2]; 

  println!("All numbers: {:?}", all_numbers_slice); 
  println!("The second of the first two numbers: {}", first_two_numbers[1]); 
} 

Tuples differ from arrays in the way that arrays are sequences of the same type, while tuple elements have varying types:

// tuples.rs 
fn main() { 
  let number_and_string: (u8, &str) = (40, "a static string"); 

  println!("Number and string in a tuple: {:?}", number_and_string); 
} 

They are useful for simple, type-safe compounding of data, generally used when returning multiple values from a function.

Vectors are like arrays except that their contents or length don't have to be known in advance. They are created with either calling the constructor Vec::new or by using the vec! macro:

// vec.rs 
fn main() { 
  let mut numbers_vec: Vec<u8> = Vec::new(); 
  numbers_vec.push(1); 
  numbers_vec.push(2); 

  let mut numbers_vec_with_macro = vec![1]; 
  numbers_vec_with_macro.push(2); 

  println!("Both vectors have equal contents: {}", numbers_vec == 
            numbers_vec_with_macro); 
} 

These are not the only ways to create vectors, and one typical way needs to be covered here. Rust defines iterators, things that can be iterated one by one, in a generic way. For instance, a program's runtime arguments are iterators, which would be a problem if you wanted to get the nth argument. However, every iterator has a collect method, which gathers all the items in the iterator into a single collection, such as a vector, which can be indexed. There's an example of this usage in the chapter's exercise.

Finally, HashMaps can be used for key/value data. They are created with the HashMap::new constructor:

// hashmap.rs 
use std::collections::HashMap; 

fn main() { 
  let mut configuration = HashMap::new(); 
  configuration.insert("path", "/home/user/".to_string()); 

  println!("Configured path is {:?}", configuration.get("path")); 
} 

Exercise - fix the word counter

Here's a program that counts instances of words in a text file, given to it as its first parameter. It is almost complete but has a few bugs that the compiler catches, and a couple of subtle ones. Go ahead and type the program text into a file, try to compile it, and try to fix all the bugs with the help of the compiler. The point of this exercise, in addition to covering the topics of this chapter, is to make you more comfortable with the error messages of the Rust compiler, which is an important skill for an aspiring Rust developer.

Try to make an effort even when things seem hopeless; every drop of tear and sweat brings you a step closer to being a master. Detailed answers to the task can be found in the Appendix section. Good luck!

// wordcounter.rs 
use std::env; 
use std::fs::File; 
use std::io::prelude::BufRead; 
use std::io::BufReader; 

#[derive(Debug)] 
struct WordStore (HashMap<String, u64>); 

impl WordStore { 
    fn new() { 
        WordStore (HashMap::new()) 
    } 

    fn increment(word: &str) { 
        let key = word.to_string(); 
        let count = self.0.entry(key).or_insert(0); 
        *count += 1; 
    } 

    fn display(self) { 
        for (key, value) in self.0.iter() { 
            println!("{}: {}", key, value); 
        } 
    } 
} 

fn main() { 
    let arguments: Vec<String> = env::args().collect(); 
    println!("args 1 {}", arguments[1]); 
    let filename = arguments[1].clone(); 

    let file = File::open(filename).expect("Could not open file"); 
    let reader = BufReader::new(file); 

    let word_store = WordStore::new(); 

    for line in reader.lines() { 
        let line = line.expect("Could not read line"); 
        let words = line.split(" "); 
        for word in words { 
            if word == "" { 
                continue 
            } else { 
                word_store.increment(word); 
            } 
        } 
    } 

    word_store.display(); 
} 

If you like extra challenges, here are a few ideas for you to try to flex your muscles a bit further:

  1. Add a parameter to WordStore's display method for filtering the output based on the count. In other words, display a key/value pair only if the value is greater than that filtering value.
  2. Since HashMaps store their values randomly, the output is also quite random. Try to sort the output. The HashMap's values method may be useful.
  3. Think about the display method's self parameter. What is the implication of not using the ampersand (&) before self?
 

Summary


You now know the design principles and the basic language features of Rust, how to install the default implementation, and how to use the Rust compiler to build your own single-file pieces of code.

In the next chapter, we will take a look at editor integrations and the Cargo tool, and build the foundation for the project that we will extend during the course of the book.

About the Author

  • Vesa Kaihlavirta

    Vesa Kaihlavirta has been programming since he was five, beginning with C64 Basic. His main professional goal in life is to increase awareness of programming languages and software quality in all industries that use software. He's an Arch Linux Developer Fellow, and has been working in the telecom and financial industry for a decade. Vesa lives in Jyvaskyla, central Finland.

    Browse publications by this author

Latest Reviews

(9 reviews total)
see my previous remark, I don't want to rate every book seperately
Ottima qualità didattica su argomenti complessi.
The books have great content and information that I needed.

Recommended For You

Book Title
Access this book, plus 8,000 other titles for FREE
Access now