Traits and Generics I
Overview
These notes provide an overview of the code examples we went over in lecture.
Here are the key takeaways:
- Most OOP languages share functionality through classes using inheritance. However, inheritance sometimes really struggles in cases where you have classes that have some characteristics of one parent class, but not all, and maybe also has characteristics of a different parent class. Rust subscribes to a different paradigm called composition where you compose a class of multiple interfaces (called “traits” in Rust).
- Traits can tell us what a type can do, allow us to override functionality, allow us to define default implementations, and let us overload operators like
+, -, *, /, ==, >, <, etc.
- You implement a trait using the “impl Trait for Class” syntax. You can also automatically derive common traits like Debug, Partial, Eq, Copy, Clone, etc.
- You can define your own traits too, as we saw with the ComputeNorm example. Defining a trait lets you define a common interface across multiple different classes (and sometimes lets you define a common implementation, too!)
- You can make your data structures generic using the
syntax (you don’t have to use the letter T as the generic type name, it’s just common practice). For example, you could define a list that takes elements of any type T (we’ll have you do this in the week 3 exercises) - You can specify trait bounds over generics to avoid rewriting code! e.g. we can implement Display for MatchingPair
for any T that already implements Display.
LinkedList
Example: Display
and Drop
impl fmt::Display for LinkedList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut current: &Option<Box<Node>> = &self.head;
let mut result = String::new();
loop {
match current {
Some(node) => {
result = format!("{} {}", result, node.value);
current = &node.next;
},
None => break,
}
}
write!(f, "{}", result)
}
}
impl Drop for LinkedList {
fn drop(&mut self) {
let mut current = self.head.take();
while let Some(mut node) = current {
current = node.next.take();
}
}
}
Deriving Traits with Point
#[derive(Debug, PartialEq, Clone, Copy)]
struct Point {
x: f64,
y: f64
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Point {x: x, y: y}
}
}
fn main() {
let the_origin = Point::new(0.0, 0.0);
let mut p = the_origin; // copy semantics!
println!("p: {:?}, the_origin: {:?}", p, the_origin);
println!("are they equal? => {}", p == the_origin);
p.x += 10.0;
println!("p: {:?}, the_origin: {:?}", p, the_origin);
println!("are they equal? => {}", p == the_origin);
}
Defining Our Own ComputeNorm
Trait and Add
pub trait ComputeNorm {
fn compute_norm(&self) -> f64 {
0.0
}
}
// The following doesn't really make sense, it's just meant to illustrate the
// idea that the functions in traits can have default implementations!
impl ComputeNorm for Option<u32> {}
impl ComputeNorm for Point {
fn compute_norm(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
impl ComputeNorm for Vec<f64> {
fn compute_norm(&self) -> f64 {
// an example of functional rust syntax
self.iter().map(|x| {x * x}).sum::<f64>().sqrt()
}
}
impl Add for Point {
type Output = Self; // an "associated type"
fn add(self, other: Self) -> Self {
Point::new(self.x + other.x, self.y + other.y)
}
}
Generics with MatchingPair<T>
and MyOption<T>
use std::fmt;
pub struct MatchingPair<T> {
first: T,
second: T
}
pub enum MyOption<T> {
Sumthin(T), Nuthin
}
impl fmt::Display for MyOption<u32> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyOption::Sumthin(num) => write!(f, "Sumthin({})", num),
MyOption::Nuthin => write!(f, "Nuthin :("),
}
}
}
impl<T> MatchingPair<T> {
pub fn new(first: T, second: T) -> Self {
MatchingPair {first: first, second: second}
}
}
// an example of "where" syntax
impl fmt::Display for MatchingPair<char> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.first, self.second)
}
}
fn main() {
let ps_in_a_pod: MatchingPair<char> = MatchingPair::new('p', 'P');
println!("two ps in a pod: {}", ps_in_a_pod);
let my_some_five: MyOption<u32> = MyOption::Sumthin(5);
let my_nuthin: MyOption<u32> = MyOption::Nuthin;
println!("my_some_five: {}", my_some_five);
println!("my_nuthin: {}", my_nuthin);
}
Trait Bounds
impl<T: fmt::Display> fmt::Display for MyOption<T> { // more general!
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyOption::Sumthin(num) => write!(f, "Sumthin({})", num),
MyOption::Nuthin => write!(f, "Nuthin :("),
}
}
}
impl<T> fmt::Display for MatchingPair<T> where T: fmt::Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.first, self.second)
}
}