Type-Driven design in Rust
Helio
Posted on April 12, 2024
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:
This code will generate a progress bar like this:
So what will happen when we using a unbounded list like:
The output will be like below one:
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:
Nice. The first step we should think how to create a progress bar like tqdm, so we write down the following code:
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:
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:
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:
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()
}
All right, let we create a struct named Progress
, and define it like below one:
Ok, what if we design Progress use like this?
for n in v.iter().progress() {
expensive_cal()
}
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 }
}
}
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`
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 }
}
}
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>,
}
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)
}
}
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,
}
}
}
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()
}
The last concept I want to show you is Type-State
, we write the code like this one:
The display
will be impl like this one:
The rest part:
Finally we impl the Type-State
version of Progress.
Posted on April 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.