"Type-Driven API Design (Rust)" - my note

Page content

I followed this YouTube video to learn “Type-Driven API Design”.

This note was created during review so that I could extract/summarize knowledge I didn’t know before.

Define your custom trait for all types

In this video, the author want to implement progess function for a type which is an associated type Item of iterator. By using impl<T> MyTrait for T, you can implement the trait for all types in its scope. In other words, you can implement fluent interfaces to 3rd party libraries by trait for generic type T.

struct Progress<T, U> {
    iterator: T,
    i: usize,
    bound: U,
}

trait ProgressIteratorExt: Sized {
    fn progress(self) -> Progress<Self, Unbounded>;
}

impl<T> ProgressIteratorExt for T {
    fn progress(self) -> Progress<Self, Unbounded> {
        Progress::new(self)
    }
}

By v.iter().progress(), He created a new Progress object which contains iterator. Suppose you are Python programmer. You can not implement progress for the returned type of iter unless you change the implementation of iter.

progress returns the type “unbounded Progress” Progress<Self, Unbounded>, and if we want to create a bounded Progress, with_bound turns Unbounded into Bounded.

Trait “inheritance”

It was described in the official document (19.2).

Let’s look at the trait std::iter::ExactSizeIterator:

pub trait ExactSizeIterator: Iterator {
    fn len(&self) -> usize { ... }
    fn is_empty(&self) -> bool { ... }
}

The first line (trait definition) says, “if you want to implement this ExactSizeIterator trait, the struct must have Iterator trait also”, or “you can implement ExactSizeIterator only the the struct is implemented Iterator”.

This notation can be regarded as trait inheritance, but it just inherits a specification (trait), not implementations.

Implement Iterator

When you want to implement Iterator trait for your type, you need to define what the associated type Item is and how next method behaves (because next is the requried method as its documentation says).

impl<T> Iterator for Progress<T>
where T: Iterator { 
    type Item = T::Item;

    fn next(&mut self) -> Option<Self::Item> { 
        println!("{}{}", CLEAR, "*".repeat(self.i));
        self.i += 1;
        self.iterator.next()
    } 
}

Tips

Magic string: \x1B[2J\x1B[1;1H

const CLEAR: &str = "\x1B[2J\x1B[1;1H";
println!("{}", CLEAR);

This is called ANSI escape code.

  • \x1b is ASCII for ESCAPE (ESC key), and [2J is control sequence to clear entire display.
  • \[1;1H is also a control sequence for cursor opsition (meaning of H).
  • \[i;jH means “move the cursor to row i, column j

When to introduce generic/trait

If you think your function can be generaized, abstracted, irrelavant to the function name.

Reminder: Rust supports higher-order function

Refer to my quick note about function pointer.

fn progress<T>(v: Vec<T>, f: fn(&T)){

HashSet

https://doc.rust-lang.org/rust-by-example/std/hash/hashset.html

Consider a HashSet as a HashMap where we just care about the keys (HashSet<T> is, in actuality, just a wrapper around HashMap<T, ()>).

“What’s the point of that?” you ask. “I could just store the keys in a Vec.”

A HashSet’s unique feature is that it is guaranteed to not have duplicate elements. That’s the contract that any set collection fulfills. HashSet is just one implementation. (see also: BTreeSet)