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.
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.
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 buildfor just compiling without running, andcargo checkfor quickly checking if your code compiles without producing an executable.
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
mutwhen 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.
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:
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
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’scharis 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! Thecups as f64converts the integercupsto a floating-point number so we can multiply it byprice_per_cup.
You can also use the return keyword for early returns:
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.
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:
use std::io;brings theiomodule into scopeString::new()creates a new, empty Stringio::stdin().read_line(&mut name)reads a line from standard input and stores it inname- we’ll look into what&mutmeans in the next chapter.expect("Failed to read line")handles potential errors (we’ll learn better error handling later).trim()removes the newline character from the input
Note: We use
let mut namebecauseread_lineneeds 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.
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:
-
We define a helper function
read_numberthat handles reading and parsing integers. This makes our code more reusable. -
We define
calculate_revenuethat takes the number of cups and price, and returns the total revenue. Note that we convertcupstof64usingas f64to match thepricetype. -
In
main, we prompt the user for the number of cups sold. -
We calculate and display the results.
This is a simple but complete program that demonstrates the concepts we’ve learned so far.
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.
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.