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:
- trait Speak: This defines a trait named
Speak
. This asks types to implement a methodspeak
that returns aString
. It is similar to defining an interface in PHP. - &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. impl Speak for Dog
: This is how you implement a trait for a specific type. It is similar to implementing an interface in PHP.fn make_sound<T: Speak>(animal: &T)
: This is a generic function that takes a reference to any type that implements theSpeak
trait. TheT: 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 Clone
, Debug
, 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
andCat
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 map
, filter
, fold
, 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.