Rust for PHP Developers (Part 3)
We are now getting into the parts which are more and more different than PHP. We have already discussed a little bit of struct and enum.
Let’s start this part talking about “destructuring”
1. Destructuring
Destructuring is a way to unpack values from data structures. In PHP, we can do this with arrays. In Rust, we can do this with tuples, structs, and enums.
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle(i32),
Rectangle(i32, i32),
}
fn main() {
// Destructuring a tuple
let tuple = (1, 2, 3);
let (a, b, c) = tuple;
println!("a: {}, b: {}, c: {}", a, b, c); // a: 1, b: 2, c: 3
// Destructuring a struct
let point = Point { x: 1, y: 2 };
let Point { x, y } = point;
println!("x: {}, y: {}", x, y); // x: 1, y: 2
// Destructuring an enum
let shape = Shape::Circle(10);
let Shape::Circle(radius) = shape else {todo!()};// else is needed because we also have a rectangle shape
println!("radius: {}", radius); // radius: 10
// Destructuring only some values
let point = Point { x: 1, y: 2 };
let Point { x, .. } = point;
println!("x: {}", x); //
}
2. Generics
Generics are a way to write code that can work with multiple types. In PHP, generics are not supported, however there have been talks about it in the past.
In Rust, generics are a first-class citizen. We can use generics with structs, enums, and functions.
// Generics with structs
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// Generics with enums
#[derive(Debug)]
enum Shape<T> {
Circle(T),
Rectangle(T, T),
}
// Generics with functions
fn print_point<T: std::fmt::Display>(point: Point<T>) {
println!("x: {}, y: {}", point.x, point.y);
}
// A more complex example with traits
trait Shape2<V> {
fn area(&self) -> V;
}
struct Rectangle<T, U> {
width: T,
height: U,
}
impl<T, U, V> Shape2<V> for Rectangle<T, U>
where
T: std::ops::Mul<U, Output = V> + Copy,
U: Copy,
{
fn area(&self) -> V {
self.width * self.height
}
}
fn main() {
// Generics with structs
let point = Point { x: 1, y: 2 };
println!("{:?}", point);
let point = Point { x: 1.0, y: 2.0 };
println!("{:?}", point);
// Generics with enums
let shape = Shape::Circle(10);
println!("{:?}", shape);
let shape = Shape::Circle(10.0);
println!("{:?}", shape);
let shape = Shape::Circle("10");
println!("{:?}", shape);
// Generics with functions
let point = Point { x: 1, y: 2 };
print_point(point);
let point = Point { x: 1.0, y: 2.0 };
print_point(point);
// A more complex example with traits
let rect = Rectangle { width: 10, height: 20 };
println!("Area: {}", rect.area()); // Area: 200
}
Let’s go through the last example:
- We define a trait called
Shape2with a methodarea. For the time being consider Traits as interfaces in PHP. - We define a struct called
Rectanglewith two generic typesTandU. - We implement the
Shape2trait for theRectanglestruct, whereTmust implement theMultrait and be copyable, andUmust be copyable.Mulis a trait that allows us to multiply two values. - We define the
areamethod to return the product ofwidthandheight.
3. Option and Result
One of the most common patterns in Rust is the use of Option and Result types. These types are used to represent values that may or may not be present, and values that may or may not be valid.
// Option type
fn get_index_value(index: usize) -> Option<i32> {
let arr = [1, 2, 3];
if index < arr.len() {
Some(arr[index])
} else {
None
}
}
// since value will be an Option, we need to use `unwrap` to get the value, but need to ensure that it is not None
for i in 0..5 {
match get_index_value(i) {
Some(v) => println!("Value: {}", v),
None => println!("Value not found"),
}
}
The above code will print:
Value: 1 Value: 2 Value: 3 Value not found Value not found
Now let’s look at the Result type.
// Result type
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
// since value will be an Result, we need to use `unwrap` to get the value, but need to ensure that it is not an error
for i in 0..2 {
match divide(10, i) {
Ok(v) => println!("Value: {}", v),
Err(e) => println!("Error: {}", e),
}
}
The above code will print:
Error: Cannot divide by zero Value: 10
4. Pattern Matching
We saw an example of pattern matching in the previous section. Pattern matching is a powerful feature in Rust that allows us to match values against patterns. It is similar to switch statements in PHP, but much more powerful. I will quickly go through the different types of pattern matching in Rust.
// Pattern matching with enums
enum Shape {
Circle(i32),
Rectangle(i32, i32),
}
// Pattern matching with structs
struct Point {
x: i32,
y: i32,
}
fn main() {
let shape = Shape::Circle(10);
match shape {
Shape::Circle(radius) => println!("Circle with radius: {}", radius),
Shape::Rectangle(width, height) => {
println!("Rectangle with width: {} and height: {}", width, height)
}
} //output: Circle with radius: 10
let point = Point { x: 1, y: 2 };
match point {
Point { x, y } => println!("Point with x: {} and y: {}", x, y),
} // output: Point with x: 1 and y: 2
// Pattern matching with guards
let shape = Shape::Circle(10);
match shape {
Shape::Circle(radius) if radius > 5 => println!("Large circle with radius: {}", radius),
Shape::Circle(radius) => println!("Small circle with radius: {}", radius),
Shape::Rectangle(width, height) => {
println!("Rectangle with width: {} and height: {}", width, height)
}
} // output: Large circle with radius: 10
// Pattern matching if let
let shape = Shape::Circle(10);
if let Shape::Circle(radius) = shape {
println!("Circle with radius: {}", radius);
} else {
println!("Not a circle");
} // output: Circle with radius: 10
// Pattern matching with let else, needs the code to diverge
let shapes = vec![Shape::Circle(10), Shape::Rectangle(10, 20)];
for shape in shapes {
let Shape::Circle(radius) = shape else {
println!("Not a circle");
continue; // diverge, as radius is used after this
};
println!("Circle with radius: {}", radius);
} // output: Circle with radius: 10
// Not a circle
// Pattern matching with while let
let mut shapes = vec![Shape::Circle(10), Shape::Rectangle(10, 20)];
while let Some(shape) = shapes.pop() {
match shape {
Shape::Circle(radius) => println!("Circle with radius: {}", radius),
Shape::Rectangle(width, height) => {
println!("Rectangle with width: {} and height: {}", width, height)
}
}
} // output: Rectangle with width: 10 and height: 20
// Circle with radius: 10
}
What let is doing in above examples is that it is destructuring the value and binding the variables to the values i.e. shape starts to have a value of 10 from Shape::Circle(10) and then we can use it in the match statement.let else allows to assign a value to a variable if matched else goes to the else block. When I said the code needs to diverge, I meant that the code should not continue executing after the else block if the value is used after
5. Collections
Quick note on additional collections. When using PHP arrays are the go-to data structure for everything. In Rust, we have different collections for different use cases.
- HashMap: A collection of key-value pairs. e.g.
let map = HashMap::new();thenmap.insert("key", "value"); - BTreeMap: A sorted collection of key-value pairs. e.g.
let map = BTreeMap::new();thenmap.insert("key", "value");. - HashSet: A collection of unique values. e.g.
let set = HashSet::new();thenset.insert("value"); - BTreeSet: A sorted collection of unique values. e.g.
let set = BTreeSet::new();thenset.insert("value"); - BinaryHeap: A collection of values that are stored in a binary heap. e.g.
let heap = BinaryHeap::new();thenheap.push("value");What’s a BinaryHeap? It is a data structure that keeps the greatest value at the top. - VecDeque: A double-ended queue. e.g.
let deque = VecDeque::new();thendeque.push_front("value");ordeque.push_back("value");
Remember that for all collections you need to import them and you need to provide the type of the key and value. In the examples above I have not provided the type, that’s because the compiler can infer the type from the context.
That means once you do let map = HashMap::new(); and then do map.insert("key", "value"); the compiler will know that the key is a string and the value is a string and now you can’t do map.insert(1, 2); as it will throw an error.
I think this is a good place to stop. We have covered a lot of ground in this part. We have seen how to use generics, pattern matching, and collections in Rust. We have also seen how to use the Option and Result types. In the next part, we will cover some more topics like traits, iterators and closures.
