Add map! and set! macros to std

I think this doesn't need a RFC. Two things regarding API compatibility:

  • A module in Rust can contain items and macro items. Currently, non-macro items and macro items are differentiated. Playground and something like use foo::bar imports both bar macro and bar function
  • Previous programs that use map! and set! aren't broken.

It's just an addition of vec!-like macros to std to improve readability a bit:

  • map! (From<([K, V])>)
  • set! (From<[T]>)
#![feature(decl_macro)]

pub macro map {
    () => {
        {
            From::from([])
        }
    },
    ($($key:expr => $value:expr),+ $(,)?) => {
        {
            From::from([$(($key, $value)),+])
        }
    }
}

Since there's no default type, both map! and set! will require a contextual type. It looks like this:

// no need to put HashMap into scope!
Data {
    other_data: map! {
        "k" => "v",
    },
}

vs.:

// need to put HashMap into scope!
Data {
    other_data: HashMap::from([
        ("k", "v"),
    ]),
}
2 Likes

I’d suggest you slow down on your rate of opening new topics. It’s getting a bit too much at once. We’re at number 5 within less than 30 hours.

8 Likes

Ok

1 Like

The thing is, these can just as well live outside of std. What specific benefit does having them in std confer? What makes map![(a, b), (c, d)] so much better than <_>::from_iter([(a, b), (c, d)]) or [(a, b), (c, d)].into() to justify its inclusion into every program ever? Of in effect taking the map! name away from library implementations, since it's generally a poor idea to shadow std macros?

Of still generally needing to use let type ascription to guide type inference? Thus limiting the terseness available to most cases:

let x = HashMap::from([...]);     // std
let x: HashMap<_, _> = map![...]; // new

Of needing to explain why _: HashSet<_> = map![(a, b), (c, d)] and _: HashMap<_, _> = set![(a, b), (c, d)] work? Or them producing a Vec<_> or any other FromIterator unrelated to maps/sets?

I have used a collect![$($item),*] macro before, and think it could be a reasonable addition to std. map! or set! with the same implementation as collect!, not so much.

8 Likes

For one, style, in realistic code rustfmt makes arrays of tuples very verbose and difficult to read (which is why the macro doesn't use tuples)

HashMap::from_iter([
    (
        dimension::Kind::Overworld,
        Dimension {
            persistent: vec![PersistentArea::Square { .. }],
        },
    ),
    (
        dimension::Kind::Nether,
        Dimension {
            persistent: vec![PersistentArea::Square { .. }],
        },
    ),
])

map![
    dimension::Kind::Overworld => Dimension {
        persistent: vec![PersistentArea::Square { .. }],
    },
    dimension::Kind::Nether => Dimension {
        persistent: vec![PersistentArea::Square { .. }],
    },
]

Should it be in the prelude? We have prior art of stable namespaced macros now.

I'm not so sure that's true, of the 32 results for rg HashMap::from_iter in my source folder 19 of them don't need any inference guidance (they could be written <_>::from_iter, and they don't use explicit annotations), the other 13 are all clippy testcases.

Personally I'm not going to pull in a dependency for such simple macros, but I would use them if they were in std.

3 Likes

You can use the crate 'sugars' :slight_smile:

1 Like

In most cases, you don't use let. The idea here is to use map! mostly for structure fields (S { m: map! {} }), tuple elements (G(map! {}, map! {})) and call arguments (i.e. f(map! {})), which are always typed (Rust requires you to type these, regardless of type inference).

I don't see any problem. map! is much more readable than collect! [ (K1, V1), (K2, V2), ...]

(NOT A CONTRIBUTION)

The original justification for not including these was that they could be a HashMap or a BTreeMap and there wasn't reason to prefer one or the other. I think this has not proved prescient: my experience is that users overwhelmingly prefer HashMap/HashSet unless they have a specific reason to need an ordered map or set.

I think this is a decision that has been made largely by inertia and is worth revisiting. I think especially new users would find having a macro available helpful.

13 Likes

There are other customized user map types too, I think... I'm not sure if there should be both map! and hash_map!.

As you said, HashMap is more frequent.

My experience is just one data point, but in most of my code bases BTreeMap is decidedly preferable over HashMap, the issue with the latter being that it can't be reliably hashed due to the lack of ordering. So the ordering might not always be directly important, but it is quite often indirectly important.

I don't really see the issue with just adding both btree_map! and hash_map!. It solves the HashMap/BtreeMap ambiguity issue, it's more explicit and more descriptive, and actually gives control over which kind of map to initialize, all at a grand cost of typing 5/6 more characters relative to map!.

13 Likes

I would expect a uniqueness check from such a macro. I suppose identical keys are logic errors in most use cases.

Having bmap! / bset! and hmap! / hset! could be short, more convenient names for those to get ordered and unordered maps and sets.

7 Likes

+1 — This also would help not confuse me, who use Option::map, Result::map, and Iterator::map much more often than BTreeMap (and almost never use HashMap). When I see map!, my first thought is the method, not a data structure.

Edit to add: When I see set!, my first thought is the verb "to set", as in the Lisp function set!, not a data structure.

9 Likes

Barring a const PartialEq implementation, that's impossible as tokens by themself are insufficient to establish equality.

1 Like

@sahnehaeubchen

An example for illustration: map! { next_uuid() => foo, next_uuid() => bar }. The keys look identical syntactically, but subsequent calls to next_uuid() can yield distinct values.


Although, if course, we could be discussing run-time uniqueness checks… arguably, they already happen, the default behavior for from_iter is just to keep the … eh … looking up docs… ah here… only the last value, not to panic. (And experimentation shows, that last value is paired with the first “copy” of the key).

5 Likes

I don't think it's necessary to have separate macros for Hash vs BTree. Just let type inference handle that:

let map: HashMap<_, _> = map! {
    "a" => 1,
    "b" => 2,
};
// desugars to
let map: HashMap<_, _> = [
  ("a", 1),
  ("b", 2),
].into();

You can even make it default to HashMap using the type system: playground nvm

This could justify not adding a vec![] macro though, if Rust didn't already have one (after all, you can just collect into a Vec..), right?

But more generally, who likes depending on trivia crates for small bits of syntax sugar? There's a proliferation of crates for this, like maplit, maplit2, map-macro, hashmap_macro, hmap, lit2, collections_macros, simple-collection-macros, hashmacro, sugars, helper and common_macros (the last three has.. other stuff as well), and probably a few I don't know about. Yet, people are just as likely as using some of them (which one? is one of them some kind of "standard"?) as not bothering to add yet another dependency.

In other words, crates with macros for creating maps are Rust's left-pad, and that's entirely because the stdlib doesn't provide it.

2 Likes

vec! has a narrower and clearer purpose than map!/set!. vec! creates a Vec, and can only be used to create a Vec. set! can be used to create... any FromIterator? Any From<[T; N]>? And is a much more ambiguous name, that is even used with multiple meanings within std.

hashmap!/hashset! are at least potentially viable imo since their purpose is much clearer.

left-pad is not notable for being a microlibrary for functionality that could have been std. It's notable for being a widely used (micro)library which disappeared, breaking the ecosystem. cratesio dependencies cannot be disappeared in that manner; the comparison is at best disingenuous.

3 Likes

Indeed, I would be happier with separate hashmap!{} and btreemap!{} in std than with a generic map!{}; in practice we don't really need a map literal that is generic on the data structure that often.

But alas, the issues for it (here and, earlier, here) are from 2014, and it's unlikely that anything has changed to make those macros a more pressing need in the last 9 years. If anything, the From impl seems to be good enough for a lot of people (not me; just like Vec::from([1, 2, 3]) doesn't feel good enough) so I think demand may have actually decreased.

But I like to think that this issue was just something that fell through the cracks, and adding hashmap/hashset/btreemap/btreeset macros to stdlib might still be possible, even though it addresses basically a papercut at this point.

Fair enough, but my memories around the incident centered on how people shouldn't even be depending on microlibraries like left-pad to begin with. One issue is that, unless you stick to dependencies made by well known authors, each dependency adds to the number of different people that you need to trust (and that could potentially wreak havoc if their crates.io credentials were compromised)

3 Likes