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.
// 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 struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 }; let Point { x, y } = point; println!("x: {}, y: {}", x, y); // x: 1, y: 2 // Destructuring an enum enum Shape { Circle(i32), Rectangle(i32, i32), } let shape = Shape::Circle(10); let Shape::Circle(radius) = shape; println!("radius: {}", radius); // radius: 10 // Destructuring only some values let point = Point { x: 1, y: 2 }; let Point { x, .. } = point; println!("x: {}", x); // x: 1
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 struct Point<T> { x: T, y: T, } let point = Point { x: 1, y: 2 }; let point = Point { x: 1.0, y: 2.0 }; // Generics with enums enum Shape<T> { Circle(T), Rectangle(T, T), } let shape = Shape::Circle(10); let shape = Shape::Circle(10.0); let shape = Shape::Circle("10"); // Generics with functions fn print_point<T: std::fmt::Display>(point: Point<T>) { println!("x: {}, y: {}", point.x, point.y); } 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 trait Shape<V> { fn area(&self) -> V; } struct Rectangle<T, U> { width: T, height: U, } impl<T, U, V> Shape<V> for Rectangle<T, U> where T: std::ops::Mul<Output = V> + Copy, U: Copy, { fn area(&self) -> V { self.width * self.height } } 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
Shape
with a methodarea
. For the time being consider Traits as interfaces in PHP. - We define a struct called
Rectangle
with two generic typesT
andU
. - We implement the
Shape
trait for theRectangle
struct, whereT
must implement theMul
trait and be copyable, andU
must be copyable.Mul
is a trait that allows us to multiply two values. - We define the
area
method to return the product ofwidth
andheight
.
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), } 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 // Pattern matching with structs struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 }; match point { Point { x, y } => println!("Point with x: {} and y: {}", x, y), } // 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); } // 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), } }
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.