Crate rustcomp

source ·
Expand description

Adds comprehensions to Rust. This is achieved through a functional macro, rcomp!, that does all the heavy lifting for you.

Basic Usage

The core idea is simple: provide an easy and concise way to flatten, map, filter, and collect iterators. A basic comprehension looks like this:

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

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<_>>();
assert_eq!(v, it);

Now we’re getting somewhere! You can also map the values, so let’s double the values:

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<_>>();
assert_eq!(v, it);

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

Note on Collecting

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<_>>();
assert_eq!(v, it);

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<_>>();
assert_eq!(v, it);

Nesting

Nested iterators are supported up to the recursion limit by chaining the 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);
       }
    }
}
assert_eq!(v, it);

Advanced Examples

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

Note on Examples

It’s important to note that iterator examples used to test the comprehensions are equivalent to the comprehensions, but not identical. The macros expand 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.

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 only 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.

Macros

  • Generates an iterator that yields the results of the comprehension. The syntax allows for flattening, filtering, mapping, and collecting iterators (in that order).