Rust
Terms
- Fat Pointer Contains Address actual data and length
Cargo
Sample file
[package]
name = "hello_world"
version = "0.0.1"
authors = [ "Iain Wiseman iwiseman@bibble.co.nz" ]
Sample commands
cargo new hello_world --bin
cargo build
cargo run
Fundamental Data Types
Primitive types
Cam declare with size of type
let a:u8 = 123; // unsigned int 8 bits number immutable
let a:i8 = 123; // signed int 8 bits number immutable
let mut a:u8 = 123; // unsigned int 8 bits number mutable
Or without e.g.
let mut c = 123456789 // 32-bit signed i32
println!("c = {}", c);
Now variable based on OS e.g.
let z:isize = 123 // signed 64 bit if on 64 bit OS
Decimal
let e:f64 = 2.5 // double-precision, 8 bytes or 64-bits
Char
let x:char = 'x' // Note 4 bytes unicode
boolean
let g:bool = false; // Note 4 bytes unicode
Operators
Does not support -- and ++ but does support
a -= 2;
Remainder can be calculated using
a%3
Bitwise
let c = 1 | 2 // | OR
Shift
let two_to_10 = 1 << 10; // 1024
Logical of standard e.g.
let pi_less_4 = std::f64::consts::PI < 4.0; // true
Scope and shadowing
Curly braces keep scope
fn test()
{
{
let a = 5;
}
println!("Broken {a}");
}
Shadowing is fine though
fn test()
{
let a = 5;
{
let a = 10;
println!("10 {a}");
}
println!("5 {a}");
}
Constants
Standard const
const MEANING_OF_LIFE:u8 = 42;
Static const
static Z:i32 = 123;
Stack and Heap
Same a c++ i.e.
let y = Box::new(10);
println!("y = {}", *y);
Types
Tuples
Eezy peezy lemon squeezy
fn sum_and_product(x:i32,y:i32) -> (i32, i32)
{
(x+y, x*y)
}
fn main()
{
let sp = sum_and_product(3,4);
let (a,b) = sp;
let sp2 = sum_and_product(4,5);
// combine
let combined = (sp, sp2);
let ((c,d), (e,f)) = combined;
}
Arrays
Array sizes cannot grow in rust
Simple
let mut a:[i32;5] = [1,2,3,4,5];
// Or
let mut a = [1,2,3,4,5];
// Length
a.len()
// Assignment
a[0] = 321
// Printing
println!("{:?}", )
// Testing
if a == [1,2,3,4,5]
{
}
// Initialise
let b = [1,10]; // 10 array initialised to 1
Multi Dimension
Here is a two dimension array
let mtx:[[f32;3];2] =
[
[1.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
];
Slices
A slice is a non-owning pointer to a block of memory. For example
// Create a vector
let v: Vec<i32> = {0..5}.collect();
// Now create a slice (reference)
let sv: &[i32]= &v;
// We create a slice with only some elements
let sv1: &[i32]= &v[2..4];
// Printing these will produce the same result
println!("{:?}",v);
println!("{:?}",sv);
// And the range
println!("{:?}",sv1);
Get the first 3 elements of an array
fn use_slice(slice: &mut[i32])
{
}
fn test()
{
let mut data = [1,2,3,4,5];
// Passes element 1-3 to use_slice as a reference
use_slice( &mut data[1..4]);
}
Strings
Basic String
let name = String::from("Iain");
Two types, static string and string type
let s = "hello";
// Cannot do
// let h = s[0]
// You can iterate as a sequence using chars e.g.
for c in s.chars()
{
println!("{}", c);
}
And now the mutable string in rust essentially an vector // Create a string
let mut letters = String::new();
Add a char
let a = 'a' as u8;
letters.push(a as char);
String to str
let u:%str = &letters;
Concatenation
let z = lettters + &letters
Other examples
let mut abc = "hello world".to_string()'
abc.remove(0);
abc.push_str("!!!");
abc.replace("ello","goodbye")
Hashmap
Reminds me of my C++ and Java days. No surprises here for reference
let mut basket = HashMap::new();
basket.insert(String::from("banana"), 2);
basket.insert(String::from("pear"), 2);
basket.insert(String::from("peach"), 2);
Updating was a bit more tricky than expected. This was the copilot approach
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new();
for line in results.lines() {
let mut split_iterator = line.split(',');
// NOTE: We use `unwrap` because we didn't deal with error handling yet.
let team_1_name = split_iterator.next().unwrap();
let team_2_name = split_iterator.next().unwrap();
let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap();
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// TODO: Populate the scores table with the extracted details.
// Keep in mind that goals scored by team 1 will be the number of goals
// conceded by team 2. Similarly, goals scored by team 2 will be the
// number of goals conceded by team 1.
let team_1 = scores.entry(team_1_name).or_insert(TeamScores::default());
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;
let team_2 = scores.entry(team_2_name).or_insert(TeamScores::default());
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;
}
scores
}
The suggestion was to use get_mut on hashmap but struggle to get this to work. The solution from Chris biscardi on youtube was this, clearly the rust team looked at this and did it better.
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
...
scores
.entry(team_1_name)
.and_modify(|team: &mut TeamScores| {
team.goals_scored += team_1_score;
team.goals_conceded += team_2_score;
})
.or_insert(TeamScores {
goals_scored: team_1_score,
goals_conceded: team_2_score,
});
scores
.entry(team_2_name)
.and_modify(|team: &mut TeamScores| {
team.goals_scored += team_2_score;
team.goals_conceded += team_1_score;
})
.or_insert(TeamScores {
goals_scored: team_2_score,
goals_conceded: team_1_score,
});
Control Flow
if statement
Same as C++ except no brackets
if temp > 30
{
println!("Blah");
}
else if temp < 10
{
println!("Blah");
}
else
{
println!("Blah");
}
Elvis is like
let a = if temp > 30 {"sunny"} else {"cloud"}
While and Loop
While
Same as C++ except no brackets
while x < 1000
{
}
There is support for continue and break
Loop
Loop is while true
loop
{
if y == 1 << 10 { break; }
}
For Loop
A bit like kotlin loops (I think)
for x in 1..11
{
println!("x = {}",x);
}
You can get position in series as well
for (pos,x) in (1..11).enumerate()
{
println!("x = {}, pos = {}",x, pos);
}
Rust Principles
Ownership
Move
Move is when you assign a value to another variable. If we try and use a variable after the move we will get an error.
let v = vec![1,2,3]
let v2 = v;
println!("{:?}",v2)
println!("{:?}",v) // Error
Copy
When we copy something me make a new thing. They is not the same a let a = b, which is assignment. Copy means we duplicate the underlying data of the type. For primitives a copy is implemented by default. This is because the primitive has a know size. E.g. u32, bool etc. If you want to be able to copy a non primitive you need to add the derive macro. Note Clone must also be specified
#[derive(Copy, Clone)]
enum Direction {
North,
East,
South,
West,
}
#[derive(Copy, Clone)]
struct RoadPoint {
direction: Direction,
index: i32,
}
Clone
Clone is a method you can call on a struct if you want a second instance and not move the ownership. Here is an example. The struct obviously needs to implement the Copy/Clone macro. Cloning clearly increases the memory used.
let v = vec![1,2,3]
let v2 = v.cone();
println!("{:?}",v)
println!("{:?}",v2)
References
So references are like C++ references, but for rust this means you can pass the ownership during function call
main() {
let mut s = String::from("Hello");
change_string(&mut s);
}
fn change_string(some_string: &mut String) {
some_string.push_str(", world!");
}
Note for returning a Reference
If we are returning a reference we must be returning a parameter as all local variables are destroyed. (Clearly Rust is not going to allow new MyMemory(6502)
Structs
General
There are 3 types of structs, name, tuple and unit structs
- Named
- Tuples
- Unit
Name Struct
struct User
{
active: bool,
username: String,
sign_in_count: u32
}
let user1 = User{active: true, username: String::from("Biil"),
sign_in_count: 0};
println!("{}", user1.username);
...
fn build_user(username: String) -> User {
User {
username,
active:true,
sign_in_count: 1
}
}
Tuple Struct
Tuple structs use the order in which declared to assign.
struct Coordinates{i32,i32,i32};
let coords = Coordinates{1,2,3};
Unit Struct
These are used to mark the existence of something
struct UnitStruct;
let a = UnitStruct{}
The example shown was when you are implementing a trait (interface) but the properties were not required for this type. So given a trait for Area, Square uses size but Point does not have an area as it is zero
trait AreaCalculator {
fn calc_area(&self) => f64
}
struct Square {
size: f64
}
struct Point;
impl AreaCalculator for Square {
fn calc_area(&self) -> f64 {
self.size * self.size
}
}
impl AreaCalculator for Point {
fn calc_area(&self) -> f64 {
0.0
}
}
We can use it for error
struct DivideByZero;
fn divide(nom: f64, den: f64) -> Result<f64, DivideByZero> {
if den != 0.0 {
Ok(nom/den)
} else {
Err(DivideByZero)
}
}
Example Structs
struct Point
{
x: f64,
y: f64
}
fn main()
{
let p = Point { x: 30.0, y: 4.0 };
println!("point is at ({},{})", p.x, p.y)
}
Methods on Structs
Methods on struct require the first argument to be self
Example Method
Add method len to struct
struct Line
{
start: Point,
end: Point
}
// Declare impl using the keyword impl. Not ends with no semi colon.
impl Line
{
fn len(&self) -> f64
{
let dx = self.start.x - self.end.x;
let dy = self.start.y - self.end.y;
(dx*dx+dy*dy).sqrt()
}
}
Changing an attribute
To change an attribute and ensure you do not break the borrowing rules we do
struct Square {
width: u32,
height: u32
}
impl Square
{
fn area(&self) -> u32 {
self.width * self.height
}
fn change_width(&mut self, new_width: u32) -> Self
{
self.width = new_width;
}
}
...
main() {
...
let mut sq = Square(width:5, height: 5);
sq.change_point(10)
}
Lifetime
What are Dangling References
The code below will not compile. This is because x goes out of scope before r. I am guessing this is what is known as a dangling reference.
fn test() {
let r;
{
let x = 5;
r = &x; // Error `x` does not live long enough
}
log::info!("{}",r);
}
Lifetime Annotations
Not sure which way around these are but you specify lifetime annotations on functions and structs and they imply information to the compiler on how long the parameters will live for.
Three Rules of Lifetimes
Here are the rules but we also need to understand what they apply to. Kind of chicken and egg. An example is give below which is broken because these rules are not followed.
- Each Parameter that is a reference gets its own lifetime parameter
- If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- If there are multiple input lifetime parameters, but one of them is &self or &mut self the lifetime is assigned to all output lifetime parameters
Example (Broken code)
Here is an example of code which cannot be compiled without lifetime being specified.
pub struct TestStruct {
length: i32,
}
fn test2(x: &TestStruct, y: &TestStruct) -> &TestStruct { // Missing lifetime specifier
if x.length > y.length {
x
}
else {
y
}
}
Adding Annotations
To do this we specify annotations. The extension in vscode does this for us using the quick fix. The code now looks like this
fn test2<'a>(x: &'a TestStruct, y: &'a TestStruct) -> &'a TestStruct {
if x.length > y.length {
x
}
else {
y
}
}
My inference from this is that all parameters have the same lifetime.
Lifetime Annotations for Structs
Structs can also have lifetime annotations. If you specify a reference then you will need to specify a lifetime annotation. In the example below when we make the struct of type MyString we need to make sure that str1 does not go out of scope while x of type MyString exists otherwise it would refer to something no longer in scope.
// Without lifetime annotation will not compile.
// struct MyString {
// text: &str,
// }
struct MyString<'a> {
text: &'a str,
}
fn main() {
let str1 = String::from("This is my String);
let x = MyString(text: str1.as_str());
}
Static Lifetimes
We can also have lifetimes for statics.
let s: &'static str = "I live forever";
Doing this means the values are stored in the binary.
Enums
Example 1 with Method
Seems a bit C++ but...
enum Pet {dog, cat, fish}
And now lets add a method as we do with structs. Note for this method we are returning something and with rust all locals are destroyed on return so we need to specify a lifetime.
enum Pet {dog, cat, fish}
impl Pet {
fn what_am_i(self) -> &'static str {
match self {
Pet::dog => "I am a dog",
Pet::cat => "I am a cat",
Pet::fish => "I am a fish",
}
}
}
Example 2
enum Color {
Red,
Green,
Blue
}
fn main()
{
let c:Color = Color::Red;
match c
{
Color::Red => prinln!("Color is Red");
Color::Green => prinln!("Color is Green");
}
}
Example 3 with Types
enum Color {
Red,
Green,
Blue,
RgbColor(u8,u8,u8) // Tuple
CmykColor{cyan:u8, magenta:u8, yellow:u8, black:u8,} // Struct
}
fn main()
{
let c:Color = Color::RgbColor(10,0.0);
match c
{
Color::Red => prinln!("Color is Red");
Color::Green => prinln!("Color is Green");
Color::RgbColor(0,0,0) => prinln!("Color is Black");
Color::RgbColor(r,g,b) => prinln!("Color is {},{},{}", r,g,b);
}
let d:Color = Color::CmykColor(cyan:0, magenta:0, yellow:0, black:0);
match d
{
Color::Red => prinln!("Color is Red");
Color::Green => prinln!("Color is Green");
Color::RgbColor(0,0,0) => prinln!("Color is Black");
Color::CmykColor(cyan:_, magenta:_, yellow:_, black:255) => prinln!("Black");
}
}
Option<T> Enum
This enum if provided for us by rust and looks like this
enum Option<T> {
None,
Some(T)
}
We would choose this type when we have a case where there could be a value or not. I guess this is the equivalent of string? in Typescript where we may or may not have a value. In rust we use match to support this type.
let some_number = Some(5);
let some_string = Some("a string");
let nothing: Option<i32> = None;
Pattern Matching
Match is Exhaustive approach to pattern matching. I.E. you need to specify something for every option you are using match for. However you can include a default. I find this a great approach
Examples
Simple Match
match x
{
0 => "zero"
1 | 2 => "one or two"
9...11 => "lots of" // two dots does not include end value (exclusive)
_ if(blahh) => "something"
_ => "all others"
}
Here is another example.
let country = match country_code
{
44 => "uk",
46 => "sweden",
7 => "russia"
1...999 => "unknown" // other triple dot does include end value (inclusive)
_ => "invalid" // invalid
};
This just shows inclusive which is ..= unlike kotlin which I think is 3 dots
// This function returns how much icecream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no icecream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23.
fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
match hour_of_day {
0..22 => Some(5),
22..=23 => Some(0),
_ => None,
}
}
More Complex
Stumped me when see thing for the first time prior to type script and possibly lambda. Here we define anonymous functions which match the type of the enum. Here is the enum which is used in another struct
enum Message {
Move(Point),
Echo(String),
ChangeColor(u8, u8, u8),
Quit,
Resize { width: u64, height: u64 },
}
It has functions for each enum type.
struct State {
width: u64,
height: u64,
position: Point,
message: String,
// RGB color composed of red, green and blue.
color: (u8, u8, u8),
quit: bool,
}
impl State {
fn resize(&mut self, width: u64, height: u64) {
self.width = width;
self.height = height;
}
fn move_position(&mut self, point: Point) {
self.position = point;
}
fn echo(&mut self, s: String) {
self.message = s;
}
fn change_color(&mut self, red: u8, green: u8, blue: u8) {
self.color = (red, green, blue);
}
fn quit(&mut self) {
self.quit = true;
}
fn process(&mut self, message: Message) {
...
}
}
At first I struggled to understand how to implement process but all you need to do is provide an ()_=> {} for each type. For Quit I completely understood but for the others was confused. Obvious once you know and I am sure copilot will do this for me
fn process(&mut self, message: Message) {
match message {
Message::Move(point) => self.move_position(point),
Message::Echo(output) => self.echo(output),
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
Message::Quit => self.quit(),
Message::Resize { width, height } => self.resize(width, height),
}
}
Match on Tuples
This is an exert from [Game of Life]. We can match on tuples, and I imagine other types too. For tuples you can specify a value or compare to a value. Note the use of otherwise
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
Operators and Symbols
Found in Table B-1 here [Operators and Symbols]
- [Range]: 1..10
- [RangeFrom]: 1..
- [RangeTo]: ..10
- RangeFull: ..
- RangeInclusive: 1..=10
- RangeToInclusive: ..=10
Option <T> and if let
Used to avoid null or invalid values. This was used in things where the value might be present. Maybe command line arguments where some were provide or none were provided. Lets to the classic divide by zero.
let x = 3.0
let y = 0.0 // Divide by zero
let result:Option<f64> =
if y != 0.0 { Some(x/y) } else { None };
// Using match
match result {
Some(z) => println!("Goody result"),
None => println!("No result")
}
// Using if let
if let Some(z) = result { println!("z = {}", z); }
More if let
Here is another example
let mut stack = Vec:new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
while let
The above example makes great sense but while doing rustlings the was this question
// TODO: Make this a while-let statement. Remember that `Vec::pop()`
// adds another layer of `Option`. You can do nested pattern matching
// in if-let and while-let statements.
integer = optional_integers.pop() {
assert_eq!(integer, cursor);
cursor -= 1;
}
I did like the Some Some approach
while let Some(Some(integer)) = optional_integers.pop() {
assert_eq!(integer, cursor);
cursor -= 1;
}
But could not get the You can do nested pattern matching in if-let and while-let statements to look nice
while let Some(integer) = if let Some(integer) = optional_integers.pop() {
integer
} else {
None
} {
assert_eq!(integer, cursor);
cursor -= 1;
}
Just wouldn't let it lie, found a way to turn it up the right way
while let Some(integer) = optional_integers.pop() {
if let Some(integer) = integer {
assert_eq!(integer, cursor);
cursor -= 1;
}
}
Generics
Simple
This is very similar to C++ Templates and TypeScript Generics
struct Point<T>
{
x: T,
y: T
}
fn generics()
{
let a:Point<i32> = Point {x: 0, y: 4}
}
Using Implementation
Must the same, just need good examples and we a well away
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
}
Traits
Traits are similar to interfaces in java and c#
Defining a Traits
trait Animal
{
fn create(name:&'static str);
fn name(&self) => &'static str;
fn talk(&self)
{
println!("{} cannot talk",self.name());
}
}
Implement a Trait
Here we create a struct which will implement out trait. Note we do not have to implement all functions if the trait provides a default implementation
Implement a Trait for Animal
struct Human
{
name: &'static str;
}
impl Animal for Human
{
fn create(name:&'static str) -> Human
{
Human{name: name}
}
fn name(&self) -> &'static str
{
self.name
}
// override default
fn talk(&self)
{
println!("{} can talk",self.name());
}
}
Implement a Trait for Cat
Here we implement the Animal Trait for Cat
struct Cat
{
name: &'static str;
}
// Implement interface
impl Animal for Cat
{
fn create(name:&'static str) -> Cat
{
Cat{name: name}
}
fn name(&self) -> &'static str
{
self.name
}
// override default
fn talk(&self)
{
println!("{} says meeow",self.name());
}
}
// Usage
let h:Human = Animal::create("John");
let c:Cat = Animal::create("John");
Default Trait and Spread
For a struct we can create a default for it. We can use a typescript like spread operator (although it must be last) for override these defaults
pub struct Circle {
color: String,
point: Point,
radius: u16,
}
impl Circle {
pub fn new(color: String, point: Point, radius: u16) -> Circle {
Circle {
color,
point,
radius,
}
}
pub fn default_color(point: Point, radius: u16) -> Circle {
Circle {
point,
radius,
..Default::default()
}
}
}
impl Default for Circle {
fn default() -> Self {
Circle {
color: String::from("black"),
point: Point::new(0, 0),
radius: 0,
}
}
}
// Default Circle
let circle = Circle::default();
// Default Black Circle
let circle = Circle::default_color(Point::new(1, 1), 1);
Traits and Impl
To allow any struct which implements the trait we use the dyn keyword
trait Licensed {
fn licensing_info(&self) -> String {
"Default license".to_string()
}
}
struct SomeSoftware;
struct OtherSoftware;
impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}
// TODO: Fix the compiler error by only changing the signature of this function.
fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool {
software1.licensing_info() == software2.licensing_info()
}
// Now we can do this
compare_license_types(SomeSoftware, OtherSoftware)
compare_license_types(OtherSoftware, SomeSoftware)
Provided Traits
Drop Trait
Drop trait is called automatically to free up resources but you can write your own e.g. for the example above we could write
impl Drop for Course {
fn drop(&mut self) {
println("Dropping")
}
}
Clone Trait
Like the drop trait we can implement our own. Refer to the clone trait for this.
Copy Trait
We can either specify #[derive(Copy, Clone)] or implement our own. There are restrictions on this
From and Into Trait
This allow us to convert from one type to another
fn into(self) -> T
fn from(T) -> Self
fn try_into(self) -> Result<T, Self: Error>
fn try_from(value: T) -> Result<Self, Self: Error>
Trait Bounds 1
In order to allow use of more than on trait in a function we can use the +. This example means that item must implement both traits, i.e. SomeTrait and OtherTrait
fn some_func(item: impl SomeTrait + OtherTrait) -> bool {
item.some_function() && item.other_function()
}
Trait Bounds 2
Here is an example of doing the same thing in two ways. Because we can have anything in grade (T) we must make an implementation for std::fmt::Display. That way if we make a ReportCard with a generic which does not support Display, it will not compile
struct ReportCard<T> {
grade: T,
student_name: String,
student_age: u8,
}
// Approach 1
impl<T> ReportCard<T>
where
T: std::fmt::Display,
{
fn print(&self) -> String {
format!(
"{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade,
)
}
}
// Approach 2
impl<T: std::fmt::Display> ReportCard<T> {
fn print(&self) -> String {
format!(
"{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade,
)
}
}
Trait Bounds
The example above has two ways to achieve the same thing. If we constrain what this allowed, this is called trait bounds. Lets add a second parameter.
// This example only forces the struct to implement the trait
// fn overview(item1: &imp Overview, item2: &imp Overview)
// But this force the struct to be of the same type
// fn overview<T: Overview>(item1: &T, item2: &T)
We can add more constraints with the + operator. Now they need the second trait.
// fn overview(item1: &imp Overview + AnotherTrait, item2: &imp Overview + AnotherTrait)
// fn overview<T: Overview + AnotherTrait>(item1: &T, item2: &T)
Here we have an example of ensuring that the incoming parameters are constrained to be of type T
struct Pointy<T> {
x: T,
y: T,
}
impl <T> Add for Pointy <T>
where T: Add<Output = T>
{
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
Passing Trait as Parameters
So here is an example of two structs with overview implement,one using the trait default implementation, the other its own. We can use the trait similar to a pointer to a function.
Example
trait Overview {
fn overview(&self) -> String {
format("This is a rust course")
}
}
struct Course {
headline: String,
author: String
}
struct AnotherCourse {
headline: String,
author: String
}
impl Overview for Course {
}
impl Overview for AnotherCourse {
fn overview(&self) -> String {
format("{}, {}", self.author, self.headline)
}
}
We can use the overview trait a a fn parameter with
fn call_overview(item: &imp) {
println("Overview: {}", item.overview())
}
// OR
fn call_overview<T: Overview>(item: &T) {
println("Overview: {}", item.overview())
}
Passing Traits (From Youtube)
Taken from Youtube and repeated. There are two notations for passing a trait. These are the same but the first is perhaps more readable. The second is known as a trait bound.
pub fn foo (traitor: &impl SpiDevice) {
}
pub fn foo<T: SpiDevice>(traitor: &T) {
}
With the impl syntax we can make the parameter have more the one trait with a plus.
pub fn foo (traitor: &impl SpiDevice + AnotherTrait) {
}
With the second syntax if we have two parameters if allows us to make sure they both share the same trait easily as the type is only specified once
pub fn foo<T: SpiDevice>(traitor1: &T, traitor2) {
}
We can also add a second trait with this syntax too.
pub fn foo<T: SpiDevice + AnotherTrait>(traitor1: &T, traitor2) {
}
This starts to get messy to we can tidy this up with the Where Clause
pub fn foo<T, U>(traitor1: &T, traitor2: &U) -> i32
where
T: SpiDevice + AnotherTrait,
U: AnotherTrait + YetAnotherTrait
{
42
}
Returning Traits (From Youtube)
We can also return traits but you cannot return different types which share the same trait at this time.
pub fn foo() -> SpiDevice {
// Must be of same type
}
Common Collections
Vectors
Same a c++
let mut a = Vec::new()
a.push(1);
a.push(2);
a.push(3);
// Print
println!("a[0] {}", a[0]);
// We can create vector with initial capacity
let mut b = Vec::<i32>::with_capacity(2);
// We can initialize using an iterator values of 0-4
let c: Vect<i32> = (0..5).collect();
// Using get returns a option
match a.get(3333)
{
...
}
// Removing, pop returns an option
let last_elem = a.pop();
// Using the option type iterating over vector to print it
while let Some(x) = a.pop()
{
println!("x = {}",x);
}
Binary Heap
This make sure the highest is at the top. It has a peek function to allow you to peek at values.
let mut bHeap = BinaryHeap::new();
bHeap.push(1);
bHeap.push(18);
bHeap.push(20);
bHeap.push(5);
bHeap.pop();
println!("{:?}", bHeap); // 20
Maps
Not discussed
Sets
Not discussed
Error Handling
Panic
Panic happens when unhandled error occurs. This happens for instance when we access out of bounds array. We can get a backtrace by setting the environment export RUST_BACKTRACE=-1
Result Enum
The Result an enum which has two generics Result<T, E> where T is the type an E is the error. In rust we use the match to determine what to do.
let file = File::Open("Does_not_exist.mp3");
let file match file {
Ok(file) => file,
Err(error) => panic("Error: {:?}", error),
};
Mapping Errors
Rust likes you to make your own errors and map the ones you handle to you errors which makes sense. We make our own errors using enums
enum ParsePosNonzeroError {
Creation(CreationError),
ParseInt(ParseIntError),
}
Now we can provide helper function to convert from one type of error to ours
impl ParsePosNonzeroError {
fn from_creation(err: CreationError) -> Self {
Self::Creation(err)
}
// TODO: Add another error conversion function here.
fn from_parse_int(err: ParseIntError) -> Self {
Self::ParseInt(err)
}
}
Now in our parse function we can map the errors in parse() to our own
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
0 => Err(CreationError::Zero),
x => Ok(Self(x as u64)),
}
}
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// TODO: change this to return an appropriate error instead of panicking
// when `parse()` returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
}
}
Testing
We specify the cfg option and use the assert library
fn sqrt(number: f64) -> Result<f64, String> {
if number >= 0.0 {
Ok(number.powf(0.5))
} else {
Err("negative floats don't have square roots".to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sqrt() -> Result<(), String> {
let x = 4.0;
assert_eq!(sqrt(x)?.powf(2.0), x);
Ok(())
}
}
We can run these with
cargo test
Handling CLI Arguments
Clap seems to be the approach for this, make sure you install the macros otherwise errors will show on the derive macro.
cargo add clap --features derive
And the code. The shows how to replace text from a file to another file.
use regex::Regex;
use text_colorizer::*;
use clap::Parser;
use std::fs;
#[derive(Parser,Debug)]
struct Args {
#[clap(short, long)]
pattern: String,
#[clap(short, long)]
replace: String,
#[clap(short, long)]
input_file: String,
#[clap(short, long)]
output_file: String,
}
fn replace (pattern: &str, replace: &str, data: &str) -> Result<String, regex::Error> {
let regex = Regex::new(pattern)?;
Ok(regex.replace_all(data, replace).to_string())
}
fn read_write_file(args: Args) {
let data = match fs::read_to_string(&args.input_file) {
Ok(data) => data,
Err(err) => {
eprintln!("{} failed to read from file {}: {:?}",
"Error".red().bold(),
args.input_file, err);
std::process::exit(1);
}
};
let replace_data = match replace(&args.pattern, &args.replace, &data) {
Ok(data) => data,
Err(err) => {
eprintln!("{} failed to replace text {:?}",
"Error".red().bold(),
err);
std::process::exit(1);
}
};
match fs::write(&args.output_file, replace_data) {
Ok(_) => {},
Err(err) => {
eprintln!("{} failed to write to file {}: {:?}",
"Error".red().bold(),
args.output_file, err);
std::process::exit(1);
}
};
}
fn main_body(args: Args) -> Result<(), ()> {
// Your main body here
println!("input_file {}", args.input_file);
println!("output_file {}", args.output_file);
read_write_file(args);
Ok(())
}
fn main() {
let args = Args::parse();
main_body(args).unwrap_or_else(|_| {
eprintln!("Error");
std::process::exit(1);
});
}
Closures
Closures are functions you which you can use the available scope to with that function. They look like anonymous function in typescript
Mapper Function using Closure
let a = 10;
let a = 10;
let a = 10;
Here is a closure which is like typescript mapper function using closures and rust