SemVer compatibilty of exported declarative macros using 3rd party dependencies

(the context)

I have a macro that looks like this

macro_rules! hash_map {
    {$($k: expr => $v: expr),* $(,)?} => {
        <::hashbrown::HashMap::<_, _> as ::core::iter::FromIterator<_>>::from_iter([$(($k, $v),)*])

and allows you to instantiate a hashbrown::HashMap like this:

let m = hash_map! {
    "en" => "Hello",
    "de" => "Hallo",
    "fr" => "Bonjour",
    "es" => "Hola",

Should I re-export hashbrown and use it in my macro like $crate::hashbrown::HashMap instead of ::hashbrown::HashMap or can I avoid it?

My thought process revolves mostly[1] around the fact that I'd have to add hashbrown as a dependency, which it doesn't have to be (I'm not using it in the crate; for tests I can add it as a dev-dependency). Adding it as a dependency will cause predictable SemVer compatibility version conflicts[2] between my crate and hashbrown, but also restricts the compatibility and requires more maintenance (not too bad with dependabot, but still).

As it currently stands, the macro I presented above is de facto compatible with every version of hashbrown ever released. I know de jure I can't rely on that being the case for ever, but it is oh so tempting. My macro could be incompatible with a future version of hashbrown that'd remove the FromIterator implementation. If that'd happen, I assume it would be a very confusing error for the user.

In summary, I believe it'd be more correct to add hashbrown as a dependency, but I'm really tempted not to do it because I believe it is highly unlikely that my assumption of compatibility with future versions of hashbrown is going to break (but I wouldn't have a safety net SemVer provides me with—even though it might be unnecessarily restrictive in my case). I'd be really happy to hear your opinion on this matter.

Here a summary of my thoughts:

hashbrown as dependency hashbrown not as a dependency
Pros Predicable SemVer incompatibility between my crate and hashbrown De facto compatibility with every version of hashbrown at once
Cons Macro only works with one SemVer compatible version of hashbrown Potential for unpredictable compatibility breakage

  1. I also thought about people renaming their hashbrown dependency, but right now I'm more interested in the question about de facto vs de jure compatibility with hashbrown ↩︎

  2. I.e. currently it'd work with SemVer compatible versions of 0.14 of hashbrown, but if they were to release hashbrown v0.15 or hashbrown v1, you couldn't use the macro with these versions. And older versions wouldn't be supported either ↩︎

I agree with you that it seems undesirable to reexport a specific version of hashbrown.

If you take out the mention of HashMap entirely, then it's just a FromIterator call with an unknown result type, which can work with many different kinds of maps. So, I would suggest that you provide that version of the macro, which is 100% future-proof.

You can then offer a separate macro which wraps it and constrains the result type:

($($args:tt)*) => {
    let map: ::hashbrown::HashMap<_, _> = map!($($args:tt)*);

or perhaps even, instead of trying to pin down the type to a crate, just mention unqualified HashMap and lets that be whatever type the caller imported. (A violation of macro hygiene, but one that can't truly be avoided, and might be useful.)

1 Like

I don’t really see the downside. You also mention a hazard, but what’s the actual hazard?[1] Hashbrown changes its interface in a future version and your macro thus doesn’t support this version of hashbrown… so what? A macro crate doesn’t have to be compatible with all versions of a crate… one version, or a specific set of versions, all should be completely fine. The only issue I can think of would be that your crate can’t document in advance what the compatible versions are going to be. But you can just claim it’s all versions up to (current date) and likely most future versions, too.

(The other issue is that it relies on hashbrown being named hashbrown [and a dependency of the user’s crate in the first place][2], but that’s an assumption that many proc-macro crates do as well, right?)

  1. There’s only possible breakage for your users if they upgrade their hashbrown dependency in a breaking way. They make a breaking upgrade and get breakage. I see no hazard. ↩︎

  2. whereas a macro_rules macro using a re-export via $crate::… can work entirely without any explicit dependency, or with arbitrarily renamed dependency, on the user’s end ↩︎

1 Like

This is beautiful. It'll be a great enhancement to the crate's API, thanks a lot!

There are two things I did consider hazardous[1] about it, (I) exactly what you're saying, namely that there isn't a better way to document the fact that the macro might stop being compatible with the latest hashbrown release than writing it in the docs and (II) the error message in case breakage happens.

In case I want to import two crates with incompatible dependencies (say hashbrown v0.15 and map_macro v0.3 depending on hashbrown v0.14), I usually go to their or check out their Cargo.toml files and see what version is required. If my macro crate doesn't depend on hashbrown, this workflow wouldn't work and I'd be left wondering why I can't use the macro with hashbrown.

And concerning the latter I thought it'd be more confusing to get an error along the lines of

 = help: the trait `FromIterator<(&str, &str)>` is not implemented for `hashbrown::HashMap<_, _, _, _>`

rather than—what I'd expect when working with incompatible versions of the same crate—an error like

    = note: expected struct `hashbrown::HashMap<_, _, _, _>`
               found struct `hashbrown::HashMap<_, _, _, _>`

(Having just written that I feel slightly silly because the first error is far better than the second. The second is just what I'm used to.)

Well, I at least have made that assumption before and AFAICT others do, too. Rather than pre-emptively supporting renamed dependencies (which is a rather niche thing I'd say) I hope people wanting this as a feature would raise an issue. So I consider both someone renaming hashbrown and someone using map-macro to build hashbrown maps without explicitly having hashbrown as a dependency so unlikely that I'd still prefer—unless users explicitly want it as a feature, of course—not to add hashbrown as a dependency and re-export it.

  1. After reading your post the word hazardous feels way too strong :sweat_smile:. Unidiomatic would've probably been the better word here. Edit: I amended the OP to make it sound less dramatic. ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.