পাঠ ২০.২

Advanced Trait

Advanced Traits

Chapter 10-এ trait-এর basic দেখেছ। এখন কিছু advanced ফিচার — associated type, default generic parameter, operator overloading, fully qualified syntax, supertrait, এবং newtype pattern।

Associated type

Associated type trait-এর সাথে একটা type placeholder যুক্ত করে — trait-এর method signature-এ সেটা ব্যবহার করা যায়। Implementor বলে দেয় কোন concrete type এই placeholder-এর জায়গায়।

সবচেয়ে চেনা উদাহরণ — Iterator trait:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Generic-এর সাথে পার্থক্য

Generic দিয়ে লিখলে — Iterator<T> হত, এবং প্রতিটা impl-এ T annotate করতে হত। তাছাড়া একই type-এ একাধিক implementation possible (Iterator<String>Iterator<u32>) — কোনটা ব্যবহার হচ্ছে তা বোঝাতে annotation দরকার।

Associated type-এ — একটা type-এ trait শুধু একবারই implement। Item-এর জন্য একটাই choice; type infer স্বাভাবিক।

Default generic parameter — operator overloading

Generic declaration-এ default concrete type দেওয়া যায়: <PlaceholderType=ConcreteType>। বড় ব্যবহার — operator overloading।

std::ops::Add trait implement করে + overload:

use std::ops::Add;

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

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

Add trait-এর definition:

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

Rhs default Self। তাই impl Add for Point মানে Point + Point

ভিন্ন type-এ যোগ — Rhs override:

use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Default type parameter-এর দু'টা মূল ব্যবহার:

  1. Existing code না ভেঙে type extend করা।
  2. যেই customization বেশিরভাগ user-এর দরকার নেই — সেটাকে optional রাখা।

একই নামে একাধিক method — disambiguation

Rust-এ একই type-এ একাধিক trait-এ একই নামের method থাকতে পারে — এমনকি direct impl-এও:

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}

person.fly() default-এ direct impl-এর method কল করবে। Trait-এর method কল করতে trait নাম স্পষ্ট করো:

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
This is your captain speaking.
Up!
*waving arms furiously*

Fully qualified syntax

Method-এ &self থাকলে উপরের syntax কাজ করে। কিন্তু associated function (যাতে self নেই) — Rust কোনটা বুঝবে জানে না:

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
A baby dog is called a Spot

Animal-এর baby_name চাইলে fully qualified syntax:

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
A baby dog is called a puppy

General form:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Supertrait

কখনো নিজের trait-এ অন্য trait-এর functionality দরকার — তাহলে implementor-কে দু'টোই implement করতে হবে। সেই depended-on trait-কে বলে supertrait

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}

OutlinePrint: fmt::Display — যে কোনো type-এ OutlinePrint implement করতে হলে আগে Display চাই। তাই inside method-এ self.to_string() available।

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

External trait + external type — newtype pattern

Orphan rule মনে আছে? trait বা type — অন্তত একটা local হতে হবে। তাহলে Vec<String>-এ Display implement করা যাবে না — দু'টোই external।

সমাধান — newtype pattern। Tuple struct-এ wrap করে আমাদের local type বানিয়ে নাও:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}

Runtime overhead নেই — wrapper compile time-এই বিলীন। তবে downside — Wrapper-এ Vec<T>-এর method available না। চাইলে দরকার-মতো manually delegate, বা Deref implement করো।

এই পাঠ থেকে যা শিখলে

  • Associated type — trait-এর সাথে একটা type placeholder; per-impl একবারই বলা যায়।
  • Default generic parameter দিয়ে operator overloading; default থাকলে user-কে annotate করতে হয় না।
  • একই নামের method disambiguate — Trait::method(&val); self-নেই function-এ <Type as Trait>::function()
  • Supertrait — নিজের trait-এর implementor-কে অন্য trait-ও implement করতে বাধ্য।
  • Newtype pattern — orphan rule এড়িয়ে external trait external type-এ implement; runtime খরচ শূন্য।