Implement AnyCalendar using a macro that improves robustness
This was pulled out of #7012. I considered that PR to be pushing AnyCalendar complexity over the edge to justify a new macro, but @robertbastian asked for it to be considered separately. I disagree with a process that forces authors to split stylistic changes out of otherwise-mergeable PRs, especially given time zone differences, but let's discuss the process in another thread.
Examples of bugs this can prevent:
- Consistently applying the Hijri tabular algorithm equality check in the match
- Matching on the same calendar identity between AnyCalendar and DateInner (for example, avoiding Buddhist AnyCalendar with Coptic DateInner)
- Preventing calling a function with the same return type as the main function. For example, days_in_month and months_in_year have the same signature, so a compiler error wouldn't catch when they are mixed up.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.
| Feature | Command | Description |
|---|
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
In the other thread @robertbastian said
It's the same situation as the macro overload in the datetime crate.
I've seen you tweak the datetime crate and what would be a 100-line bug-prone change becomes a 3-line change with the macro. I used to dislike complex macros, too, but the extra time it takes for a developer to learn the macro is time saved in code review and bug reports.
This particular macro is not nearly as complex as the datetime macro, and based on @Manishearth's consistent nudging to document internal APIs, I think it's not hard to learn to use, either.
The macro could be made substantially simpler if Rust ever allows macros to generate match arms.
/gemini review, wdyt?
The refactoring is a great improvement, making the code more concise and maintainable.
Once these issues are addressed, this will be a solid improvement to the codebase.
Gemini wants to be my friend, clearly. I wonder if it would say the same thing for the opposite PR. We can test it later.
I plan to look at this, but not until this afternoon unfortunately.
So I've been thinking about this.
We basically have a macro complexity-utility tradeoff.
Most of our macros, including the current-on-main AnyCalendar match macro have very little complexity: they are basically just "copy paste this template" potentially with some very minor bells and whistles. Those tend to be easy to accept.
Beyond such simple macros, I think "complexity" comes in a couple flavors (labels are not intended to be precise):
- Use-site comprehension: Is it hard to understand what the macro is doing when you see a use site? This can be ameliorated by docs. Complex macros should pair with a sufficient amount of docs so that someone does not need to reach into the macro def to understand a use site.
- Def-site comprehension (Debuggability): Is it hard to understand what the macro is doing, regardless of docs, because you are tracking down a bug and need to reach into the macro guts?
- Reuse: Is it hard to reuse the macro in a new situation? It should not be the case that someone intending to use the macro needs to dig into the macro implementation to figure out how to use it.
- Upgradability: Is it possible to tweak the macro when requirements change? This depends on how the requirements may change, too. Maybe the macro implements a trait that will eventually get more methods. Maybe the macro produces a match statement that will eventually get more arms. Maybe we don't expect the macro to ever be edited again. Judging this requires having a good ability to predict that.
Utility also comes in a couple flavors:
- Boilerplate reduction: Large piles of repetitive code are just generally dispreferred. Getting rid of that is great. Most macros do this.
- Readability: A macro can make use sites much more readable: sometimes the important bits can often be hidden behind all the boilerplate, and it's hard to see what is and isn't really key to a particular chunk of code. A macro can be used to bring that out.
- Mistake prevention: Centralizing things in a macro makes it easier to avoid copy-paste errors and other such things.
Okay, so let's look at, say, the field_type! (use) macro.
-
Use-site comprehension: Okayish: you can basically guess what is happening here by looking at the use site, but ideally
field_type!()would have more docs that help here. - Def-site comprehension: It's got some messy bits but it's mostly just a template.
- Reuse: Super easy to reuse. Wish it had docs, again, but it's easy enough to tease out what it means
- Upgradability: This macro has been refactored a million times and has changed the precise boilerplate it generates a bunch. Pretty straightforward
And in the utility column, it ticks all of the boxes too: the short macro call makes it easy to see what's important (Readability), and probably prevents mistakes (Mistake prevention).
What about a more complex macro, impl_date_marker! (use)? This does a lot of stuff
- Use-site comprehension: Okayish. It's hard to know what exactly is happening uder the hood. This would be ameliorated by docs. It's mostly possible to squint at a couple use sites and figure out what you need to do.
- Def-site comprehension: Not great. It's got a lot of "optional" options that are messy to handle in macros.
- Reuse: Okayish, again ameliorated by docs. Copying an existing use and tweaking it works well.
- Upgradability: Unsure! It's mostly a pile of more macros, which have similar complexity. I think you would have to spend some time looking through this macro to make most of the types of changes we can expect here.
And utility wise, it's reducing a lot of boilerplate, and is amazing at bringing the important bits to the forefront both for readability and mistake prevention.
Alright, let's apply that here. Note that I'm not trying to come up with a rubric that "gives us an answer", I'm just trying to come up with a rubric that brings to the forefront the key points about a particular macro to aid this discussion.
-
Use-site comprehension: The shortcut macros are quite easy to understand, and again some docs on them would make this even better.
match_cal_generalis decently documented. - Def-site comprehension: Not great. It's a higher-order macro which already complicates things
- Reuse: Super easy to reuse. Copy paste a use site.
- Upgradability: The most common upgrade will be adding a calendar, which is easy. There's a chance we will need matches that have natures not supported by this macro, that would be much harder, but I also don't really think that's likely.
And from the utility column, it's again reducing a lot of boilerplate. However I'm not sure if there's much of a readability delta: the code it's replacing is, to me, not super hard to understand at a glance. The thing I mentioned before of using rustfmt::skip so we're not wrapping overlong lines would also be sufficient to fix that problem for me.
I think it has a slight benefit in Mistake prevention since it would prevent getting the match arms wrong, but for only two additional callsites compared to the status quo I'm not too worried, and Rust forces us to add the right match arms anyway because of crate-internal exhaustiveness. There's also a slight benefit with making sure everyone handles the HijriTabular sighting type.
With this framing, personally I think I see this macro as having a fair amount of additional complexity without much additional utility. I weakly believe that the utility does not justify the added complexity.
I appreciate being able to review this as a separate change, it's easier to see the now-more-advanced add/until code with and without the macro.
I've spent a good 10 minutes trying to understand how this works and have given up. This change is an immense readability hurdle; this code should be kept simple enough for any contributor to update.
My expectation was 3-5 minutes to learn how to use the macro. The macro is intended to be general enough that you won't normally need to modify it. If you do need to modify it, 10-15 minutes would be my expectation.
For example, I wouldn't dare to understand how std::format! works under the hood, but it is easy for me to use, given sufficient documentation.
I also don't see the type of bug that you're saying this avoids. Please update the PR description with why this PR is justified, not just where this code was previously discussed.
Added.
We basically have a macro complexity-utility tradeoff.
Yes, I agree with @Manishearth's characterizations.
Also, we should normalize passing macros as arguments to other macros. It's a powerful technique that, while not the first thing you learn in Rust, greatly improves macro brevity and readability for those who are similarly initiated. We shouldn't discourage its use due to unfamiliarity.
(no promises, but I have that topic flagged as material for a blog post.)
Also, we should normalize passing macros as arguments to other macros. It's a powerful technique that, while not the first thing you learn in Rust, greatly improves macro brevity and readability for those who are similarly initiated. We shouldn't discourage its use due to unfamiliarity.
I think it's often hard to keep track of the "signature" of macros in your head, and higher-order macros make this worse since you now need to keep track of that signature while keeping track of other stuff.
It's still a powerful and useful trick, though.
Also, we should normalize passing macros as arguments to other macros.
No we should not. It requires uncoditional trust in the macro, because it's impossible to follow what's happening.
I think it's often hard to keep track of the "signature" of macros in your head, and higher-order macros make this worse since you now need to keep track of that signature while keeping track of other stuff.
Many macros don't even have a single signature, they have dozens.
If you do need to modify it, 10-15 minutes would be my expectation.
That is not a reasonable time to expect someone to onboard onto this otherwise trivial code.
Examples of bugs this can prevent:
- Consistently applying the Hijri tabular algorithm equality check in the match
ok
- Matching on the same calendar identity between AnyCalendar and DateInner (for example, avoiding Buddhist AnyCalendar with Coptic DateInner)
- Preventing calling a function with the same return type as the main function. For example, days_in_month and months_in_year have the same signature, so a compiler error wouldn't catch when they are mixed up.
both of these things we have successfully managed for 5 years. we have code reviews, tests, and we're not stupid
Superseded by #7312