Add map! and set! macros to std part II

Here's my tuppence, picking up the sadly closed part I:

Introducing new global macros like map! and set! being a breaking change, edition 2024 would be a good moment.

While I don't mind a Perl-style mapping with =>, it's a pity that it's different from the Debug output with a JSON-style :. I wonder what would break if that were allowed after an :expr.

I'm trying to make the macros generic, with the most common variant being the default, where the context doesn't hint at a specific collection.

Since I can't push an alternate From with a default for T under the different map types, I was thinking about something like the following. I seem to be stuck on the fact that I don't know how to express that T must be of the form T<K, V>. As the Rust maps don't share a trait that has these generics, it would have to be done here.

struct MapFrom<K, V, const N: usize, T = std::collections::HashMap<K, V>>(
    PhantomData<(K, V, T)>
);
impl<K, V, const N: usize, T> MapFrom<K, V, N, T>
where
    K: Eq + std::hash::Hash,
{
    fn from(arr: &[(K, V); N]) -> T {
        T::from(arr)
    }
}
macro_rules! map {
    ($($k:expr => $v:expr),+ $(,)?) => {{
        MapFrom::from([$(($k, $v)),+])
    }}
}

Anybody have a clue how to make this compile and work?

If so set! could also be generic over all set kinds, defaulting to HashSet. Likewise vec! should then be enlarged to queues and lists, defaulting to Vec.

We already have From<array> implementations, like From<[(K, V); N]> for BTreeMap<K, V>. Your macro could leave the destination type inferred, From::from([$(($k, $v)),+]).

9 Likes

One discussion in part I was that a let binding alone gives no context. Thus annoyingly and unlike vec! it would need to declare a type. Therefore I want this to have a default type for such cases.

A default type won't help for 99% of the cases. You want inference defaults, but they don't exist. If you want the macro to be convenient, you will have two have two, one for each map type.

But honestly, if this macro is just a shortcut for HashMap::from([("a", 1), ...]), I don't see the benefit. This expression is clear enough and does not require a lot more typing.

It can be improved a little by don't constructing an array (which LLVM may not optimize), but using with_capacity() and insert() like maplit does, but I still don't see a benefit over HashMap::from(...).

2 Likes

I don't get the 99%. Sometimes I need a map locally. I never mention its type after construction. Would a function from <T = HashMap> not use HashMap by default for let? Then Rust just needs to infer K & V.

Your shortcut argument equally applies to vec!, yet I wouldn't want to miss it! It would be lovely, if Rust were consistent in having the equivalent for all 3 categories of collections (Vec…, Map… & Set…).

No, it won't. It'll work only if you mention the type in type annotation.

First, const generics didn't work when vec![] was initially created, so we couldn't have the equivalent of Vec::from([...]).

Second, Vec is very often performance critical, and the benefit of not having to allocate an array on the stack and copy it are often worth it.

Third, vec![] mimics array initialization syntax, but maps have no equivalent.

Fourth, vec![] supports additional form: vec![elem; len], that can't be expressed nicely without a macro (it can be a function, but it will not be as nice).

2 Likes

vec![] acts as a heap-allocated dynamically-sized version of simple arrays. At the time vec![] was created, Rust didn't have const generics, and arrays were severely limited to the point of being unusable in most contexts (in many cases, they still are too limited). For that reason it was critical to have a simple convenient way to work with homogenous arrays, and vec![] fit this purpose. For this reason it also uses the same syntax as array literals, allowing easy transition between the two of them.

set![] and map![] have no such justification. Creating a map of fixed elements on its own isn't common or important enough to warrant special-case syntax. This isn't Python where you use dicts instead of proper objects, we have strong typing, structs and traits for that.

8 Likes

You're surely right about the history, but it's irrelevant to the modern Rust user. From the moment I learnt Rust, I was not in favour of singling out vectors for preferential treatment.

And I repeatedly use maps and sets for managing keyed data (certainly not as a replacement for structs with few keys known in advance.)

The question is whether my syntax above is onto something? Or is it impossible to express that the parameter must itself be generic?

1 Like

TBH, I wish that form didn't exist. Unlike the other forms, it doesn't do anything that a function can't do just as well.

Vec::repeat(elem, len) would be completely fine, and avoid the all-to-easy vec![0, 1024] typo.

5 Likes

Well it's not going away, so better wish for something positive :wink:

It does it with a nice compact syntax, where we don't have to mentally parse several tokens to get it. So I and several people in part I of the thread like it.

And since you guys are saying it manages to do it more efficiently than first constructing an array, that's an added bonus.

I don't see a reason, HashMap::from and HashSet::from are more than sufficient as constructors for those types. I also believe vec! is unnecessary since Vec::from exists.

Macros have issues with diagnostics, on my IDE, if I'm inside a macro I don't get auto-completions or code actions.

I believe macros should only be used when the language doesn't have any other way to express that behavior. Being reserved as a last measure resource.

The history is relevant for the question "vec![] exists, so why wouldn't we add similar macros for other collections". As things stand, it is entirely possible that if vec![] wasn't already stable and you proposed to add it today, it wouldn't happen. Functions are good enough to deal with the practical issues, and a slightly shorter syntax isn't generally considered a good enough justification for stdlib additions.

7 Likes

I have gotten to know Rust as a highly consistent language: good things like from, into, parse, iterators, slices… are available all over the place if possible. This is amazing!

Now you're implying history trumps consistency… This thread and part I exist because some people love vec! and want it for all collections.

@chrefr said that vec! might be a bit faster than from([…]). He dismisses this as no benefit. I totally disagree. Unless it's unsafe, any reasonable optimisation is a boon for Rust!

Integers and floats have default types. Likewise I'd like the same magic to achieve the following defaults for untyped let bindings:

  • vec! -> Vec
  • map! -> HashMap
  • set! -> HashSet

If the context mandates one of their cousins, including crates that implement the right trait, that would be chosen instead.

To make them cover all constuctors, I'd even extend them with optional parameters:

vec![capacity = 1024; …]
map!{capacity = 1024, hasher=…; …}
set!{capacity = 1024, hasher=…; …}

And finally, we could make map! match its debug output (I guess currently only possible with a proc-macro) and well known JSON-style. That would be the cherry on the cake:

map!{k1: v1, k2: v2, …}

Yea it's mostly adds confusion as to why there is a compact syntax for vec and not for others, and also there are great crates which greatly expands on it (e.g. sugars, py-comp), so maybe it's time to start the process of deprecating it.

That some API surfaces don't form a neat perfectly symmetrical flawless polyhedra crystal and look like some medieval house which had parts added over centuries is perhaps not ideal, but not a reason for deprecation. The existing one isn't flawed, we just don't need more of it. Deprecation does not allow removal since we still have to carry it for backward-compatibility. So there's no benefit but it would cause churn for a lot of people.

2 Likes

Here is what I had to say about it in one of the many previous threads on this topic. My opinion hasn't changed since then.

1 Like

Here's another option: add a from_iter! macro that accepts the following syntaxes:

let map: HashMap<_, _> = from_iter![
    a => b,
    c => d,
];
// desugars to
let map: HashMap<_, _> = FromIterator::from_iter([
    (a, b),
    (c, d),
]);

let set: BTreeSet<_> = from_iter![
    a,
    b,
    c,
];
// desugars to
let set: BTreeSet<_> = FromIterator::from_iter([
    a,
    b,
    c,
]);

This macro is usually conventionally called collect! given the correlation between FromIterator and Iterator::collect. The limitation is that this requires specialization to get the proper fixed sized collection initialization, whereas From<[_; _]> doesn't.

Where a new collection macro could maybe beat the non-macro approaches is if changing evaluation order can get better codegen than the straightforward order.

2 Likes

How is this more readable or easier to write than

let set = BTreeSet::from_iter([
    a,
    b,
    c,
]);

?

And if you can rely on type inference to infer the collection type, you can just do [a, b, c].into().

Rust's standard library is kept intentionally minimal and has a high standard for additions. Consistency on its own is not enough, you need to justify why the addition is better than the alternatives, including any macros written in user crates.

Imho the fact that there is no obvious syntax (should it be a => b? a: b? a = b?) already speaks against adding it as a stdlib macro.

1 Like

I'd say { a: b, … } is the obvious syntax, as it's both in Rust (struct initialisation) and de facto standard through JSON. Like in structs, one could even treat a lone value as a key mapping onto itself as the same value.

Alas the macro system prevents this. I'd guess serde::json! would also love to be able to have $key:expr: $value:expr. So independent of whether the vec! vs. map! imbalance is addressed, it might be good to allow the colon.

1 Like