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 method area. For the time being consider Traits as interfaces in PHP.
  • We define a struct called Rectangle with two generic types T and U.
  • We implement the Shape trait for the Rectangle struct, where T must implement the Mul trait and be copyable, and U must be copyable. Mul is a trait that allows us to multiply two values.
  • We define the area method to return the product of width and height.

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.

  1. HashMap: A collection of key-value pairs. e.g. let map = HashMap::new(); then map.insert("key", "value");
  2. BTreeMap: A sorted collection of key-value pairs. e.g. let map = BTreeMap::new(); then map.insert("key", "value");.
  3. HashSet: A collection of unique values. e.g. let set = HashSet::new(); then set.insert("value");
  4. BTreeSet: A sorted collection of unique values. e.g. let set = BTreeSet::new(); then set.insert("value");
  5. BinaryHeap: A collection of values that are stored in a binary heap. e.g. let heap = BinaryHeap::new(); then heap.push("value"); What’s a BinaryHeap? It is a data structure that keeps the greatest value at the top.
  6. VecDeque: A double-ended queue. e.g. let deque = VecDeque::new(); then deque.push_front("value"); or deque.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.

Leave a Reply

Your email address will not be published. Required fields are marked *