Cute: Python like comprehensions!

Announcing Cute.

https://crates.io/crates/cute

What is it?

A Macro for python-esque list and hashmap comprehensions.
The c! macro implements list and hashmap comprehensions similar to those found in Python, allowing for conditionals and nested comprehensions.

Examples

#[macro_use(c)]
extern crate cute;

let squares = c![x*x, for x in 0..10];
let even_squares = c![x*x, for x in 0..10, if x % 2 == 0];
et squares_hashmap = c!{key => key*key, for key in 0..10};

Nested Comprehensions

let nested = vec![vec![1,2,3], vec![4,5,6], vec![7,8,9]];
let flat: Vec<usize> = c![x, for x in y, for y in nested];
assert_eq!(flat, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);

With conditions

let nested = vec![vec![1,2,3], vec![4,5,6], vec![7,8,9]];
let even_flat: Vec<usize> = c![x, for x in y, for y in nested, if x % 2 == 0];
assert_eq!(even_flat, vec![2, 4, 6, 8]);

Comprehensions over Iterators

let vec: Vec<i32> = vec![-4, -2, 0, 2, 4];
let output: Vec<i32> = c![x*2, for x in vec.iter()];
assert_eq!(output, vec![-8, -4, 0, 4, 8]);

Hashmap Comprehensions

let v: Vec<(&str, i32)> = vec![("one", 1), ("two", 2), ("three", 3)];
let map = c! {key => val, for (key, val) in v, if val == 1 || val == 2};

let mut expected: HashMap<&str, i32> = HashMap::new();
expected.insert("one", 1);
expected.insert("two", 2);

assert_eq!(map, expected);
28 Likes

c!. So what is the status for namespacing macros?

3 Likes

Cool, it reminds me of this crate:

https://github.com/goandylok/comp-rs

2 Likes

This is indeed cute, and more compact than the equivalent iterator expressions in some cases. Yet another little bit of choice to delight or confuse, depending on temperament! But it feels like time to talk about this tendency for crate names to be cute rather than meaningful. Would it not be so much more discoverable if this crate was called python-list-comprehensions?

4 Likes

Very nice! I have to agree with with @stevedonovan though that the name is a bit non-saying.

I agree with you guys. The name doesn't say a lot! However a quick search of python list comprehensions on https://crates.io/ will lead you to this crate. I guess it's not entirely hard to get it on crates.io

4 Likes

Something like this should go in the Rust prelude or standard library. When you have nesting using flat_map with the current Rust iterators is hard to write and hard to read. While equivalent Python comprehensions are a breeze to write and read.

I don't like the "c" name of the macro, it's too much short. I prefer something more descriptive like "iter" or "gen" or "generator" or "seq" or "iterator", or something like that.

The macro should generate a lazy iterator, instead of a eager vec/map, because that's more general and more efficient in some cases, and you can call a to_vec() on it (or collect()). An alternative design is to have both: "gen" is lazy and "gen_vec", "gen_hmap" are eager.

Are commas necessary? I guess you can't use "gen![x*x for x in 0 .. 10]" as in Python.

Hi Leornardo.

Thanks for the comment.

I will definitely be thinking of a more descriptive name, maybe comp! But I think that the name somehow gets out of your way, the syntax makes it easy to notice that it's a comprehension.

I will definitely be adding generator comprehensions for lazy evaluation.

Are commas necessary?

Yes. Rust places some syntactic requirements on macros. I tried to pick a delimiter that would be easy to use without deviating too much from Python's syntax.

Including "Python" in the name is pretty weird to me. Comprehensions have been implemented in many languages, and Python was not the first.

3 Likes

Sure, but Python has it pretty much nailed down. I'm not aware of any more concise implementation than what Python has provided for ages...

Since you brought up conciseness, Haskell's implementation is even more concise than Python's:

Python:

[x*2 for x in y for y in z if z < 3]

Haskell:

[x*2 | x <- y, y <- z, z < 3]

And Haskell's version is also more flexible, since it works with any monad, not just lists (or sets or generators, as in python).

But I don't think that all that matters. Comprehensions are definitely in many languages and Python didn't do them first or even in a particularly unique way.

4 Likes

I love this! I don't know if I'd use it, but I'm extremely glad it exists and that Rust macros can support it. And the name is great too. :heart:

It looks like its eagerly evaluated, is that true?

2 Likes

Thanks for the comments.

Yes It's eagerly evaluated. I'm working to add generator comprehensions (lazily evaluated)

3 Likes

Yeah, it was just a little nitpick. Otherwise it's a good crate - does one thing and is actually documented.

Sorry, but conciseness for me includes readability not just compactness as you imply. There the Python version has the clear edge.

No idea what you're trying to say here, do you mean output or input? Input in comprehensions and generator expressions can be any iterable. Output can either be a generator, a set, a dictionary or a list.

Python syntax is more readable here.

And Haskell’s version is also more flexible, since it works with any monad, not just lists (or sets or generators, as in python).

Some notes here: List comprehension - HaskellWiki

In the first versions of Haskell, the comprehension syntax was available for all monads. (See History of Haskell) Later the comprehension syntax was restricted to lists.
Since lists are an instance of monads, you can get list comprehension in terms of the do
notation.
Because of this, several Haskell programmers consider the list comprehension unnecessary now.

Funnily enough that historical page is very old (as is most stuff on the haskell wiki), since monad comprehensions were reintroduced in GHC years ago. :slight_smile:

The output in Haskell can be any monad.

Anyway, please don't get the wrong idea, I'm not at all trying to argue about the benefits of comprehensions in Haskell or any other language. If I were designing a language now I would use the Python syntax for them over the Haskell syntax, because I like words. The entire point of my comment was just to say that since Python didn't invent or even really evolve comprehensions, it doesn't need to be in the crate name. My examples in Haskell were only to show that the concepts exist in general.

1 Like

Right. But is the last phrase still correct ("several Haskell programmers consider the list comprehension unnecessary now.")? I've seen some Haskell programmers not so fond of them...

That's a matter of opinion! To a Haskell programmer or someone from a mathematical background, this is very natural and readable.

The given example [x*2 | x <- y, y <- z, z < 3] doesn't actually make sense. z is supposed to be a number, so you can't take an element of it.

Sorry, but conciseness for me includes readability not just compactness as you imply. There the Python version has the clear edge.

Haskell's notation is very, very close to set-builder notation, so for many people, including mathematicians and a good chunk of scientists, it probably is more readable. It's also a lot easier to add more terms, etc. than in Python.

2 Likes