Rust Fundamentals

Your Typical Language Introduction


Learning Objectives

  • You can write, compile, and run basic Rust programs.
  • You understand Rust’s approach to mutability and variable declarations.
  • You can use basic data types and write simple functions.
  • You can read user input and perform basic calculations.

Warming up

Let’s warm up a bit before starting. Here is a set of tiny programming tasks to get you familiar with Rust syntax. You can solve these exercises directly in the embedded code editor.

Loading Exercise...

Hello, World!

As you now know from the warming up exercises, here’s what a “Hello, World!” program looks like in Rust:

Notice the essential part:

println!("Hello, World!");

The println! macro (note the exclamation mark!) prints text to the screen.

The exclamation mark indicates that println! is a macro, not a regular function. Macros are special special constructs that generate code at compile time.

The program consists of a main function, which is the entry point of every Rust program. When you run a Rust program, execution always starts with main.

Compiling and Running

Rust is a compiled language, which means your source code must be converted to machine code before it can run. You can compile a Rust program directly using the rustc compiler:

rustc hello.rs
./hello

However, we typically use Cargo, Rust’s build tool and package manager, which makes working with Rust projects much easier.

Fighting the Compiler

Don’t worry if you make mistakes! The Rust compiler provides helpful error messages that will guide you to the solution. Rust’s motto is “fighting the compiler is part of learning Rust”; the compiler is actually trying to help you write better code.

Setting Up Your Environment

Before we continue, let’s make sure you have Rust installed and know how to work with Cargo.

Installing Rust

The recommended way to install Rust is through rustup, which manages Rust versions and associated tools. Visit rustup.rs and follow the installation instructions for your operating system.

After installation, you can verify that Rust is installed by running:

rustc --version
cargo --version

Creating a New Project with Cargo

Cargo makes it easy to create, build, and manage Rust projects. To create a new project, use the cargo new command followed by your project name:

cargo new coffee_counter
cd coffee_counter

This creates a new directory called coffee_counter with the following structure:

coffee_counter/
├── Cargo.toml
└── src/
    └── main.rs

The Cargo.toml file contains metadata about your project and its dependencies. The src/main.rs file is where you write your code. Cargo has already created a “Hello, World!” program for you.

To build and run your project:

cargo run

This command compiles your code (if needed) and runs the resulting program. During development, cargo run is the command you’ll use most often.

Note: Cargo also provides cargo build for just compiling without running, and cargo check for quickly checking if your code compiles without producing an executable.

Loading Exercise...

Working locally

Although you can run Rust code in the embedded editor, it’s highly recommended to try out the examples on your local computer. This helps you get familiar with setting up a Rust development environment, using Cargo, and working with files. As an editor, we recommend VSCode with the Rust Analyzer extension.

Variables and Mutability

In Rust, variables are declared using the let keyword. Here’s something that makes Rust different from many other languages: variables are immutable by default.

Once you’ve assigned a value to cups, you cannot change it. The following code would not compile:

fn main() {
    let cups = 5;
    cups = 6;  // Error! Cannot assign twice to immutable variable
}

If you want to change a variable’s value, you must explicitly declare it as mutable using mut:

Note: Immutability by default is one of Rust’s key features. It makes code safer and easier to reason about. You only add mut when you actually need to change a variable, making your intentions clear.

Constants

Rust also has constants, which are always immutable and must have a type annotation:

Constants use const instead of let, their names are written in UPPER_SNAKE_CASE by convention, and they can be declared in any scope, including the global scope.

Loading Exercise...

Basic Data Types

Rust is a statically typed language, which means the type of every variable must be known at compile time. Let’s look at the most common data types.

Explicit declaration of types is done using a colon : followed by the type name:

The compiler can often infer types, so you don’t always have to specify them explicitly.

Integer Types

Rust has several integer types. The most commonly used is i32, a 32-bit signed integer (which is also the default integer type):

Here’s a quick overview of integer types:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Signed integers can be negative, unsigned integers cannot. The number indicates how many bits the type uses.

Floating-Point Types

Rust has two floating-point types: f32 and f64. The default is f64:

Note: The {:.2} in the println! formats the number to two decimal places.

Boolean Type

The boolean type has two possible values: true and false:

Character Type

The char type represents a single Unicode character:

Strings and String Slices

Strings in Rust can be a bit more complex, so for now, we’ll just focus on string slices. String slices &str are references to string data:

Note: Characters in Rust use single quotes ', while strings use double quotes ". Also note that Rust’s char is a Unicode scalar value, meaning it can represent more than just ASCII.

Tuples

Tuples can group together values of different types:

Arrays

Arrays contain multiple values of the same type and have a fixed length:

The type [f64; 3] means “an array of f64 with length 3”. Arrays are indexed starting from 0.

Functions

Functions are declared using the fn keyword. We’ve already seen the main function, but let’s create our own functions.

The parameter name: &str means the function takes a string slice as a parameter. We’ll discuss the difference between strings and string slices later; for now, just know that &str is how you pass text to a function.

Return Values

Functions can return values. The return type is specified after an arrow ->:

Note: In Rust, the last expression in a function is automatically returned. Notice there’s no semicolon after cups as f64 * price_per_cup. If you add a semicolon, it becomes a statement and returns nothing! The cups as f64 converts the integer cups to a floating-point number so we can multiply it by price_per_cup.

Loading Exercise...

You can also use the return keyword for early returns:


Loading Exercise...

Expressions vs Statements

Rust distinguishes between statements and expressions.

  • Statements are instructions that perform an action but don’t return a value. They end with a semicolon.
  • Expressions evaluate to a value. They don’t end with a semicolon (in certain contexts).
fn main() {
  let x = 5;  // This is a statement

  // This is an expression that evaluates to 6
  let y = {
      let z = 3;
      z + 3  // No semicolon! This is the return value
  };

  println!("x = {}, y = {}", x, y);
}

This is why functions can return values without the return keyword - the last expression is the return value.


Loading Exercise...

Reading Input

Most programs need to interact with users. Let’s see how to read input from the keyboard using the standard library’s io module.

You can write the input that is passed to the program by pressing the cogwheel ⚙️ in the editor window. This opens up an area to which you can write the input. The input will be provided to the program when it runs. When running the program locally, you’ll type input directly into the terminal.

Let’s break this down:

  1. use std::io; brings the io module into scope
  2. String::new() creates a new, empty String
  3. io::stdin().read_line(&mut name) reads a line from standard input and stores it in name - we’ll look into what &mut means in the next chapter
  4. .expect("Failed to read line") handles potential errors (we’ll learn better error handling later)
  5. .trim() removes the newline character from the input

Note: We use let mut name because read_line needs to modify the string to add the input.

Reading Numbers

Reading numbers requires an extra step - we need to parse the string into a number:

The .parse() method attempts to convert the string to the type we specified (i32 in this case). If the conversion fails (for example, if the user types “hello” instead of a number), the program will display the error message from expect.

Loading Exercise...

Example: Coffee Counter

Let’s put everything together and create a simple program for our coffee shop. This program will ask the barista how many cups were sold and calculate the total revenue.

Let’s examine what this program does:

  1. We define a helper function read_number that handles reading and parsing integers. This makes our code more reusable.

  2. We define calculate_revenue that takes the number of cups and price, and returns the total revenue. Note that we convert cups to f64 using as f64 to match the price type.

  3. In main, we prompt the user for the number of cups sold.

  4. We calculate and display the results.

This is a simple but complete program that demonstrates the concepts we’ve learned so far.

Loading Exercise...

Expanding the Program

Let’s enhance our program to handle multiple drink types:

This expanded version tracks three different drink types. Notice how we reuse the read_number and calculate_revenue functions for each drink type. This demonstrates don’t repeat yourself (DRY), which one of the key principles of good programming.

Note: As our program grows, you might notice the repetition. In later chapters, we’ll learn better ways to handle this using collections, structs, and other Rust features.

Loading Exercise...

Summary

In this chapter, we covered the fundamentals of Rust programming:

  • How to write, compile, and run Rust programs
  • Setting up projects with Cargo
  • Variables and mutability (immutable by default!)
  • Basic data types: integers, floats, booleans, characters, string slices, tuples, and arrays
  • Writing functions with parameters and return values
  • The difference between statements and expressions
  • Reading user input from the keyboard
  • A practical example: the Coffee Counter program

These concepts form the foundation for everything else we’ll learn. In the next chapter, we’ll explore one of Rust’s most unique and powerful features: the ownership system.