As a "double agent" in Python and Rust, when working with iterables in Rust, although you can use chained methods, I find the readability isn't that great. In such cases, I really wish I could write Python-like comprehensions. That's why I created this library that extends Rust's syntax using macros.
https://crates.io/crates/better_comprehension
Of course, even if you haven't written Python before, it's fine—because comprehensions are a natural way of thinking. This library provides thorough documentation and examples, and I'm sure you'll fall in love with comprehensions.If you like this library, feel free to give it a star on github
For a better reading experience, I recommend checking out:
https://docs.rs/better_comprehension/latest/better_comprehension/
Features
- Comprehensions are provided for all containers in the standard library and provides iterator comprehension macros based on references
Vec and std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}
- More in tune with rust's syntactic design
struct Score { subject: &'static str,score: u8 }
struct Student { name: String, age: u8, scores: Vec<Score> }
let students_data = [
Student {
name: "Alice".to_string(),
age: 20,
scores: vec![
Score { subject: "Math", score: 95, },
Score { subject: "English", score: 88, },
],
},
Student {
name: "Bob".to_string(),
age: 21,
scores: vec![
Score { subject: "Math", score: 78, },
Score { subject: "English", score: 85, },
],
},
];
// use for loop
let high_scores = {
let mut high_scores = BTreeMap::new();
for Student { name, scores, .. } in &students_data {
let mut subjects = Vec::new();
for score in scores {
if score.score >= 85 {
subjects.push(score.subject);
}
}
high_scores.insert(name, subjects);
}
high_scores
};
// ↓ equivalent to the chained method call ↓
let high_scores = students_data
.iter()
.map(|Student { name, scores, .. }| {
(
name,
scores
.iter()
.filter(|score| score.score >= 85)
.map(|score| score.subject)
.collect::<Vec<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
// ↓ equivalent to the comprehension(python-like) ↓
let high_scores = b_tree_map![
name => vector![
score.subject
for score in scores.iter() if score.score >= 85
]
for Student { name, scores, .. } in &students_data
];
// ↓ more recommended in this lib ↓
let high_scores = b_tree_map![
name => subjects
for Student { name, scores, .. } in &students_data
let subjects = vector![
score.subject
for score in scores.iter() if score.score >= 85
]
];
assert_eq!(
high_scores,
BTreeMap::from([
(&"Alice".to_string(), vec!["Math", "English"]),
(&"Bob".to_string(), vec!["English"])
])
);
In the examples above you have seen that pattern matching, let expressions, and so on are compatible with rust's design, but there are many more in the library documentation
- Lower mental burden
Please note, in Rust, for loop consumes ownership.
So usually, for multi-layer loops, if you want the original collection to be consumed, you should write it like this:
use better_comprehension::vector;
let vec_1 = vec!["ABC".to_string(), "DEF".to_string()];
let vec_2 = vec!["abc".to_string(), "def".to_string()];
let vec_3 = vec![123, 456];
let vec = {
// Move the collection you want to consume into the block
let vec_1 = vec_1;
let vec_3 = vec_3;
let mut vec = vec![];
// In the outer loop, you can choose to use iter() to keep ownership
// To keep the design consistent, here we choose to use iter()
for i in vec_1.iter() {
if i == "ABC" {
// In the inner loop, you must use iter(),
// otherwise the ownership will be transferred for the first time
for j in vec_2.iter() {
if j == "abc" {
for k in vec_3.iter() {
if k == &123 {
// Only use clone when necessary to avoid unnecessary resource waste
vec.push((i.clone(), j.clone(), *k));
}
}
}
}
}
}
vec
};
// println!("{:?}", vec_1); // borrow of moved value
println!("{:?}", vec_2); // work well
// println!("{:?}", vec_3); // borrow of moved value
In this library, you don't need to do this, the macros will automatically handle these problems for you.
You only need to do two things:
- For the collection you want to keep ownership, add
.iter()
or use&
- Directly pass the variable name of the collection you want to consume
The rest will be automatically handled in the macro.
use better_comprehension::vector;
let vec_1 = vec!["ABC".to_string(), "DEF".to_string()];
let vec_2 = vec!["abc".to_string(), "def".to_string()];
let vec_3 = vec![123, 456];
let vec = vector![
(i.clone(),j.clone(),*k)
for i in vec_1 if i == "ABC"
for j in &vec_2 if j == "abc"
for k in vec_3 if k == &123
];
// println!("{:?}", vec_1); // borrow of moved value
println!("{:?}", vec_2); // work well
// println!("{:?}", vec_3); // borrow of moved value
More features are waiting for you to discover for yourself and are guaranteed to surprise you!