Rust for PHP Developers (Part 4)

The concepts starting from this part are very specific to Rust and might not always have an equivalent in PHP. However, understanding these concepts is crucial for understanding Rust.

1. Traits

Traits are all about defining behaviour. They are similar to interfaces in PHP. Traits allow you to define shared functionality that can be implemented by different types.

trait Speak {
    fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
    fn speak(&self) -> String {
        "Woof!".to_string()
    }
}

struct Cat;
impl Speak for Cat {
    fn speak(&self) -> String {
        "Meow!".to_string()
    }
}
fn make_sound<T: Speak>(animal: &T) {
    println!("{}", animal.speak());
}
fn main() {
    let dog = Dog;
    let cat = Cat;
    make_sound(&dog);
    make_sound(&cat);
}

There are several things to unpack here:

  1. trait Speak: This defines a trait named Speak. This asks types to implement a method speak that returns a String. It is similar to defining an interface in PHP.
  2. &self: This is a reference to the instance of the type that implements the trait. It is similar to $this in PHP, which is implicit in PHP, but explicit in Rust.
  3. impl Speak for Dog: This is how you implement a trait for a specific type. It is similar to implementing an interface in PHP.
  4. fn make_sound<T: Speak>(animal: &T): This is a generic function that takes a reference to any type that implements the Speak trait. The T: Speak syntax is similar to type hints in PHP.

A traits can also have default implementations:

trait Speak {
    fn speak(&self) -> String {
        "Hello!".to_string()
    }
}
struct Dog;
impl Speak for Dog {
    fn speak(&self) -> String {
        "Woof!".to_string()
    }
}
struct Cat;
impl Speak for Cat {} // Uses default implementation

// rest of the code remains the same

You can use traits defined in other crates. For example, the std::fmt::Display trait is used to format types for printing. You can implement this trait for your own types to customize how they are printed.

use std::fmt;
struct Person {
    name: String,
    age: u32,
}
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} is {} years old", self.name, self.age)
    }
}
fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };
    println!("{}", person); // Uses the Display trait
}

Some traits are already defined in the standard library, such as CloneDebug, and PartialEq. You can implement these traits for your own types to enable cloning, debugging, and equality checks.

#[derive(Clone, Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1.clone(); // Uses Clone trait
    println!("{:?}", p2); // Uses Debug trait
    assert_eq!(p1, p2); // Uses PartialEq trait
}

Trait bounds are used to specify that a generic type must implement a certain trait.

trait Speak: Display {
    fn speak(&self) -> String;
}

This means that any type that implements the Speak trait must also implement the Display trait. So our above Dog and
Cat types would need to implement the Display trait as well, otherwise the code will not compile.

2. Iterators and Closures

If you have used Laravel collections, you are already familiar with the concept of iterators and closures. Rust has a powerful iterator system that allows you to work with collections in a functional style.

let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // Output: [2, 4, 6, 8, 10]

Closures: Rust’s closures are similar to anonymous functions in PHP. They can capture variables from their surrounding environment.

let add = |x: i32, y: i32| x + y;
let result = add(5, 10);
println!("{}", result); // Output: 15

Closures can also be used with iterators:

let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
println!("{}", sum); // Output: 15

There are several iterator methods that you can use, such as mapfilterfold, and many more. These methods allow you to transform and process collections in a concise way.

When writing your own functions that take in closure, you need to understand Fn, FnMut, FnOnce.
These traits allow you to defined how a closure will be called and how it will behave.
Let me do a quick summary:

Trait Captures variables Can call multiple times? Can mutate captured vars? Consumes captured vars?
Fn By reference (&T) ✅ Yes ❌ No ❌ No
FnMut By mutable ref (&mut T) ✅ Yes ✅ Yes ❌ No
FnOnce By value (T) ❌ Only once ✅ Maybe ✅ Yes

3. Lifetimes

To be honest, lifetimes look more to me like a compiler feature than a language feature. It is a way for the Rust compiler to ensure that references are valid for as long as they are used.
I will give you an example:

struct Person<'a> {
    name: &'a str
}
fn main() {
    // failing example 
    let p = {
        let name = String::from("Alice");
        Person { name: &name } // Error: `name` does not live long enough
    };
    println!("{}", p.name);
}

In this example, the name variable is created inside a block, and its lifetime ends when the block is exited. However, the Person struct holds a reference to name, which is invalid after the block ends. 'a is a lifetime parameter that indicates Person structs’ lifetime is tied to the lifetime of the name reference.
You can avoid thinking about lifetimes in many cases by using the String type instead of &str. (owned vs borrowed)

I was thinking to cover threads and concurrency in this part, but it would be useful to talk about some more things before we reach there. So the next part would be about threads and concurrency.

Leave a Reply

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