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.rs
Rust’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.rs
Run 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 25i32
f32
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
return
If 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 if
Second 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 y
Why 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
Option
Option
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
Result
Result
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
, take
A 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 garden
src/garden.rs
src/garden/mod.rs
backyard
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 vegetables
src/vegetables.rs
src/vegetables/mod.rs
Code 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