DEV Community

Cover image for Type-Driven design in Rust
Helio
Helio

Posted on

1

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.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay