Department of Electrical Engineering, IIT Bombay
February 3, 2024
fn specifies a functionprintln! prints the string with a newline at the end; needed at the end of a statementrustc hello.rsRust’s build system and package manager
Creating a new project: cargo new hello_world
Project contents
Cargo.toml
main.rs
Run cargo build in the project directory
Output
Can be shortened to cargo b
By default, debug symbols are embedded
Use cargo build --release to get faster executable
Output
Can be shortened to cargo b -r
Execute with cargo run or cargo run -r
Project structure
multibin/
├── Cargo.toml
└── src
└── bin
├── alpha.rs
└── beta.rsRun alpha.rs with cargo run --bin alpha
Run beta.rs with cargo run --bin beta
Rust is a statically typed language
Declaring variables
let x: u32 = 5;
The type of the variable is specified after the colon :
If the type is not specified, Rust will try inferring it
The type of x is inferred as u32
The following code does not compile
Mutable variables need to be labeled with mut
Note: println!("{}", x) and println!("{x}") are equivalent
x+y inside the {}| 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 |
arch implies architecture dependent; usually 64-bit
usize is usually used to indexing arrays or other collections
An integer literal of a specific length can be specified by appending the type
5u8 or 25i32f32 and f64 are the single-precision and double-precision floating-point types
Operations between different types fail
The following code does not compile
The following code works
bool represents the boolean type which can take values true and false
char is a 4-byte type which can represent any Unicode code point
Tuple types are useful for grouping a fixed number of variables with different types
fn main() {
let x: (u8, f32, bool) = (38, 5.5, true);
// Note the {:?}
println!("{:?}", x);
println!("{} {} {}", x.0, x.1, x.2);
}Arrays are useful for grouping a fixed number of variables of the same type
returnIf a function ends with an expression and has no semicolon at the end, the expression value is returned
Example
An expression that ends with a semicolon evaluates to (), the unit type
if statementsif in a let statementIf the semicolons at the end of the if blocks are omitted, it can be used in a let assignment
All branches should return the same type
The following code does not compile
while loopfor loopSimple for loop
Syntax for including upper range limit
Iterating over the elements of a collection
Sequence of numbers f(n) for n \in \mathbb{N} such that f(n) = \begin{cases} 1 & \text{ if } n=1,2\\ f(n-1)+f(n-2) & \text{ if } n > 2. \end{cases}
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …
First attempt
match instead of ifSecond attempt
Both the if and match implementations are easy to check but repeat computations
fn fibonacci_nonrec(n: u64) -> u64 {
match n {
1 | 2 => 1,
_ => {
let mut curr = 1;
let mut prev = 1;
let mut sum = 0;
for _i in 2..n {
sum = curr + prev;
prev = curr;
curr = sum;
}
sum
}
}
}Simple tests for checking functionality of small parts of code
In Rust, unit tests can be included in the same file
Tests are run using cargo test
Unit test syntax
The same test module can have multiple unit tests
Tests can be selectively run by specifying their names (fully or partially)
Rust decides when to free unused memory during program compilation
Python and Go make this decision during program execution
Ownership rules in Rust (from Rust book Chapter 4)
The following program compiles
The following program also compiles
The following program does not compile
$ cargo run --bin string
Compiling ownership v0.1.0 (/home/sarva/rust/prog/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/bin/string.rs:4:15
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does
not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1} {s2}");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++What does it mean to implement the Copy trait?
x = y results in x being an exact copy of yWhy does String not implement Copy?
let s2 = s1.clone()Why did the following program not give an error?
"hello" is a string literal; size is known at compile timeString Assignment Changes OwnershipCloning the value is one solution
Cloning is expensive as it performs a deep copy
In many cases, references can be used to avoid ownership errors
The following program does not compile
The parameter s of the function calculate_length takes ownership of s1
s1 cannot be used in the println statement
References allow access to data values without taking ownership
Prefixing a variable with & creates a reference to it
The following program works
Creating a reference is called borrowing
Mutable references enable modifications to borrowed values
Prefixing a variable with &mut creates a mutable reference to it
The following program works
Structs allow grouping of related data
Example
self or &self, which refers to the associated structstruct with methodsimpl blockMultiple methods can be defined in the same impl block
impl blocksMultiple impl blocks can define methods of a struct
Enums (aka enumerations) allow defining a type by enumerating its possible variants
Example
enum Degree {
BTech,
MTech,
PhD,
}
struct Student {
name: String,
program: Degree,
}
fn main() {
let s = Student {
name: String::from("John Doe"),
program: Degree::BTech,
};
match s.program {
Degree::BTech => println!("{} is a UG student", s.name),
Degree::MTech | Degree::PhD =>
println!("{} is a PG student", s.name),
}
}enum CampusAddress {
Hostel(u8),
Building(String),
}
struct CampusResident {
name: String,
address: CampusAddress,
}
fn main() {
let c = CampusResident {
name: String::from("John Doe"),
address: CampusAddress::Hostel(16),
};
match c.address {
CampusAddress::Hostel(n) =>
println!("{} lives in hostel {n}", c.name),
CampusAddress::Building(s) =>
println!("{} lives in building {s}", c.name),
}
}Consider the following structs for a 2D point
They can be combined into a generic type
The type T needs to support additions and multiplications
The Num trait exactly captures this behavior
num-traits crateAdd the crate using cargo add num-traits
Cargo.toml
OptionOption is defined in the Rust standard library
Rust does not have a null pointer or reference
Option captures the notion of variables that are in one of two states: null or not null
ResultResult is also defined in the Rust standard library
It is generic over two types: T and E
Used to model the result of an operation which might fail
T is the return value on successE is the return value on errorResult exampleuse std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}greeting_file_result has type Result<std::fs::File, std::io::Error>Suppose a function implementation can encounter errors
In many cases, it is better to let the function caller handle the error
This design principle is called propagating the error
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}The ? operator is shorter syntax for error propagation
Traits are a way for different types to have the same behavior
A trait is a collection of function declarations
Example
pub trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
struct Circle {
radius: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
fn main() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
println!("Rectangle has area {}", r.area());
let c = Circle { radius: 1.0 };
println!("Circle has area {}", c.area());
}Vectors are resizable lists of same type objects
Vector initialization
Vector elements can be accessed using square brackets
Iterating over a vector using a reference
Iterating over a vector using a mutable reference
Full list at https://doc.rust-lang.org/std/vec/struct.Vec.html
len, pop, clear, insert, is_empty, resize, truncate, dedup, retain
Example
Iterators allow us to perform task on a sequence of items
Any object that implements the Iterator trait can have an iterator
Vectors have iterators
Consider the Course struct
Given a vector Vec<Course>, suppose we want to add up the credits in the core courses
fn main() {
let mut courses: Vec<Course> = vec![];
for i in 0..10 {
let c = if i % 3 == 0 {
Course {
credits: 8,
category: CourseType::Core,
}
} else {
Course {
credits: 6,
category: CourseType::Elective,
}
};
courses.push(c);
}
let core_credits: u32 = courses
.iter()
.filter(|c| c.category == CourseType::Core)
.map(|c| c.credits)
.sum();
println!("Core credits = {core_credits}");
}Suppose we want to count the number of identical components in two vectors
Solution using zip and map
cycle, count, enumerate, filter_map, find, fold, for_each, reduce, skip, takeA crate is the smallest amount of code the Rust compiler can compile
Crates can be binary crates or library crates
Binary crates have executables
Library crates have code that can be shared with multiple projects
Simple binary crate
Modules are logical partitions of the code in the project
Example of a binary crate with modules
main.rs defines a module garden
garden.rs defines a submodule vegetables
vegetables.rs defines a struct Asparagus
backyard crate
Seeing the line mod garden in main.rs, the compiler will look for module’s code in the following places
{ } block immediately after mod gardensrc/garden.rssrc/garden/mod.rsbackyard crate
Seeing the line pub mod vegetables in garden.rs, the compiler will look for module’s code in the following places
{ } block immediately after mod vegetablessrc/vegetables.rssrc/vegetables/mod.rsCode in a module is private from its parent modules by default
Seeing the line pub mod vegetables, the compiler interprets vegetables as a public module
Individual functions and types in a public module can be exposed using pub prefix