Type-Driven design in Rust

helio609

Helio

Posted on April 12, 2024

Type-Driven design in Rust

Hi. Good night. This is Helio, a Chinese student, who are learning Rust lang now. Today we gonna talk about type-driven design in Rust. This course is spring by "Type-Driven API Design in Rust" by Will Crichton, you can goto youtube to seek more details about type-driven design.

Let's look at a piece of python code now:

Bounded

This code will generate a progress bar like this:

Bounded output

So what will happen when we using a unbounded list like:

unbounded

The output will be like below one:

unbounded output

We find that if tqdm receive a bounded array, the progress bar will display, and the normal things like item/s also shown in the screen, but when tqdm receive a unbounded array like count(), it will only print the normal things like item/s, so let we design a rust lib called Progress. We will start at cargo new progress.

Start here

After enter cargo new progress, we create the project, and we run:

Hello World

Nice. The first step we should think how to create a progress bar like tqdm, so we write down the following code:

Init version

We noticed that the output is ugly, generally we will improve this, we add static CLEAR: &str = "\x1B[2J\x1B[1;1H"; at the top of the code, and output will be more fine:

Init Output

But once we need a progress bar, we also need to write this piece of code again and again, so we decided to design it to a function:

Function Version

This output exactly equal to previous one. But if we don't use the Vec<i32>, instead, we use u32, what will happend? wrong! So we normally make the Vec<i32> a Vec<T>. But if we look one more ahead, we will use the Iterator instead of Vec<T>, so the code becomes:

Iterator Version

Also the output will be equal to previous one. So if we want to go deep with the code, we should impl this one:



for n in Progress::new(v.iter()) {
    expensive_cal()
}


Enter fullscreen mode Exit fullscreen mode

All right, let we create a struct named Progress, and define it like below one:

Struct

Ok, what if we design Progress use like this?



for n in v.iter().progress() {
    expensive_cal()
}


Enter fullscreen mode Exit fullscreen mode

We need to impl something interesting:



trait ProgressIteratorExt<Iter> {
    fn progress(self) -> Progress<Iter>;
}

impl<Iter> ProgressIteratorExt<Iter> for Iter {
    fn progress(self) -> Progress<Iter> {
        Progress { iter: self, i: 1 }
    }
}


Enter fullscreen mode Exit fullscreen mode

Ext

But something went wrong, because we impl progress for every type of T, i32/u32/... Code like below one will occur errors:



let a = 10;
let p = a.progress();
for _ in p { } // Here

`{integer}` is not an iterator
the trait `Iterator` is not implemented for `{integer}`, which is required by `Progress<{integer}>: IntoIterator`


Enter fullscreen mode Exit fullscreen mode

The right version is like this:



trait ProgressIteratorExt<Iter> {
    fn progress(self) -> Progress<Iter>;
}

impl<Iter> ProgressIteratorExt<Iter> for Iter
where
    Iter: Iterator,
{
    fn progress(self) -> Progress<Iter> {
        Progress { iter: self, i: 1 }
    }
}


Enter fullscreen mode Exit fullscreen mode

The output *** is too simple, we will add delims ('[', ']') to the code, so we update our struct:



struct Progress<Iter> {
    iter: Iter,
    i: usize,
    delims: (char, char),
    bound: Option<usize>,
}


Enter fullscreen mode Exit fullscreen mode

The entire code will be like this:



#[allow(unused_variables)]
use std::{thread::sleep, time::Duration};

static CLEAR: &str = "\x1B[2J\x1B[1;1H";

struct Progress<Iter> {
    iter: Iter,
    i: usize,
    delims: (char, char),
    bound: Option<usize>,
}

impl<Iter> Progress<Iter>
where
    Iter: Iterator,
{
    pub fn new(iter: Iter) -> Self {
        Progress {
            iter,
            i: 1,
            delims: ('[', ']'),
            bound: None,
        }
    }
}

impl<Iter> Iterator for Progress<Iter>
where
    Iter: Iterator,
{
    type Item = Iter::Item;
    fn next(&mut self) -> Option<Self::Item> {
        if let Some(next) = self.iter.next() {
            match self.bound {
                Some(bound) => println!(
                    "{}{}{}{}{}",
                    CLEAR,
                    self.delims.0,
                    "*".repeat(self.i),
                    " ".repeat(bound - self.i),
                    self.delims.1
                ),
                None => println!("{}{}", CLEAR, "*".repeat(self.i),),
            }
            self.i += 1;
            return Some(next);
        } else {
            return None;
        }
    }
}

trait ProgressIteratorExt<Iter> {
    fn progress(self) -> Progress<Iter>;
}

impl<Iter> ProgressIteratorExt<Iter> for Iter
where
    Iter: Iterator,
{
    fn progress(self) -> Progress<Iter> {
        Progress {
            iter: self,
            i: 1,
            delims: ('[', ']'),
            bound: None,
        }
    }
}

impl<Iter> Progress<Iter>
where
    Iter: ExactSizeIterator,
{
    pub fn with_bound(mut self) -> Progress<Iter> {
        self.bound = Some(self.iter.len());
        Progress {
            iter: self.iter,
            i: 1,
            delims: self.delims,
            bound: self.bound,
        }
    }
}

fn expensive_cal(_n: &i32) {
    sleep(Duration::from_secs(1));
}

fn main() {
    let v = vec![1, 2, 3];
    for n in v.iter().progress().with_bound() {
        expensive_cal(n)
    }
}



Enter fullscreen mode Exit fullscreen mode

Ok, the output is [***], the same as we think. We can also make a function called with_delims:



impl<Iter> Progress<Iter>
where
    Iter: ExactSizeIterator,
{
    pub fn with_delims(mut self, delims: (char, char)) -> Progress<Iter> {
        self.delims = delims;
        Progress {
            iter: self.iter,
            i: 1,
            delims: self.delims,
            bound: self.bound,
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The output will becomes (***), right? What if we apply with_delims to unbounded array if the trait ExactSizeIterator not exist? Nothing will happen, even if the user config the delims. Ok.



for _ in (0..).progress().with_delims(('(', ')')) {
    expensive_cal()
}


Enter fullscreen mode Exit fullscreen mode

The last concept I want to show you is Type-State, we write the code like this one:

Finally Version

The display will be impl like this one:

Finally Version

The rest part:

Finally Version

Finally we impl the Type-State version of Progress.

💖 💪 🙅 🚩
helio609
Helio

Posted on April 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Type-Driven design in Rust
rust Type-Driven design in Rust

April 12, 2024