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 Shape2 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 Shape2 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),
}

// 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.

  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 *