Better_comprehension: rust syntax extend lib for Comprehension

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:

  1. For the collection you want to keep ownership, add .iter() or use &
  2. 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!

4 Likes

Looks like a good implementation of python-like compressions, and I love me a good macro :wink:

My unrelated gripes with comprehension syntax as a whole

But for me, as a person that started using python before those were a thing and using them now a lot, it inherits from them their biggest problem - wrong order of statements for reading and writing. Like, take your recommended example:

  • b_tree_map![ - gotcha map comprehension
  • name => subjects - oh, so it returns mapping of some sort, I already knew that. OK, that's not fair, there are names, but (and maybe that's only my problem) names desync from code all the time, so parser I my head ignores all the names, that I didn't see the definition of.

Only then name is defined and subjects after that. So I need to skip the "return" line in the start and get back to it in the end.

Same with writing - in python I always skip the first line of comprehension and then come back to it. Like, I could write names and define them later, but this breaks my IDE completions driven flow, and (more importantly) my head flow, that can't work with undefined variables :man_shrugging:

I blame SQL! :grin:

1 Like