#comprehension #macro #python

rustcomp

Adds vector, map, set, and iterator comprehensions to Rust

5 releases (3 breaking)

0.4.0 Oct 4, 2023
0.3.1 Oct 4, 2023
0.3.0 Oct 4, 2023
0.2.0 Oct 3, 2023
0.1.0 Oct 2, 2023

#1649 in Rust patterns

26 downloads per month

MIT license

17KB
78 lines

rustcomp

crates.io docs

Adds idiomatic comprehensions to Rust. This is achieved through a functional macro, rcomp!, that expands to iterators.

The following is modified from the documentation:

Basic Usage

The core idea is simple: provide an easy and concise way to flatten, filter, map, and collect iterators. For a full breakdown of the syntax, see the docs for the rcomp! macro. For now, consider this simple example:

let v = rcomp![Vec<_>; for x in 0..10 => x];
let it = (0..10).collect::<Vec<_>>(); // all examples show an equivalent iterator

This will make a Vec<i32> with the numbers 0 through 9... not very useful, is it? Let's add a guard to filter out the odd numbers:

let v = rcomp![Vec<_>; for x in 0..10 => x, if x % 2 == 0];
let it = (0..10).filter(|x| x % 2 == 0).collect::<Vec<_>>();

Now we're getting somewhere! You can also map the values, so let's double them for fun:

let v = rcomp![Vec<_>; for x in 0..10 => x * 2, if x % 2 == 0];
let it = (0..10)
    .filter(|x| x % 2 == 0)
    .map(|x| x * 2)
    .collect::<Vec<_>>();

Notice how the map call comes after the filter call in the iterator example. This is also how the comprehension works: the guard applies to the input value, not the output value.

Speaking of iterators, if you don't want to collect the results into a container, you can get the iterator directly by omitting the collection type:

// now we have to collect the iterator ourselves
let v = rcomp![for x in 0..10 => x].collect::<Vec<_>>();
// equivalent to:
let vv = rcomp![Vec<_>; for x in 0..10 => x];

Destructuring

Comprehensions also support destructuring. For example, tuples:

let pairs = vec![(1, 2), (3, 4), (5, 6)];
let v = rcomp![Vec<_>; for (x, y) in &pairs => x + y];
let it = pairs.into_iter().map(|(x, y)| x + y).collect::<Vec<_>>();

or structs:

struct Point {
  x: i32,
  y: i32,
}

let points = vec![Point::new(1, 2), Point::new(3, 4), Point::new(5, 6)];
let v = rcomp![Vec<_>; for Point { x, y } in &points => x + y];
let it = points.into_iter().map(|Point { x, y }| x + y).collect::<Vec<_>>();

Flattening

Flattening nested iterators is supported up to the recursion limit by chaining the for-in clauses:

let matrix = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let v = rcomp![Vec<_>; for row in &matrix, col in row => *col * 2, if *col % 2 == 0];
// nested loops are a much nicer example than iterators here
let mut it = Vec::new();
for row in &matrix {
    for col in row {
        if *col % 2 == 0 {
           it.push(*col * 2);
       }
    }
}

Advanced Examples

See the rcomp! macro documentation for some advanced examples, like creating a HashMap or HashSet.

Note on Iterator Examples

It's important to note that iterator examples used to test the comprehensions are equivalent to the comprehensions, but not identical. The macro expands to nested chains of flat_map and filter_map calls; the examples are written for clarity and to show the order of operations in the comprehension. For example, the matrix example from earlier expands to:

let v = (&matrix)
    .into_iter()
    .flat_map(|row| {
        row.into_iter().filter_map(|col| {
            if (*col % 2 == 0) && true {
                Some((*col * 2))
            } else {
                None
            }
        })
    })
    .collect::<Vec<_>>();

Notice the use of into_iter in the expansion.

What about mapcomp?

I'm aware of the existence of the mapcomp crate, but it differs from this crate in a few ways. For starters, mapcomp aims to make their syntax as close to Python as possible and I think they did a great job; this crate is not trying to do that. The goal of this crate is to add comprehensions to Rust in an idiomatic way with a syntax that flows naturally with the rest of the language while still being concise and powerful. mapcomp also provides multiple macros for different types of comprehensions while this crate provides only one.

On a more technical note, mapcomp uses generators internally which was okay for Rust 2018, but generators and yield-ing are now experimental features. This was a big inspiration for this crate, as I wanted to make a macro-based solution that didn't require nightly, so I settled on iterators in lieu of generators.

No runtime deps