Optional brackets for single-argument macros / generics

Something that came up on Discord, when we were talking about sigil type constructors...

The ability to use a macro without brackets for single-argument arms, and match the closest/smallest matching designator as its argument.

For instance:

macro_rules! Opt {
    ($t:ty) => {Option<$t>}
}

let x: Opt! Vec<i32> = Some(vec![]);

Alternatively (or additionally), just do it for single-argument generics.


let x : Vec i32 = vec![1,2,3];

fn f(x: Arc Mutex i32) {
    ...
}

The main benefit is the ability to use the parser's ability to find the end of the matched pattern instead of trying to find the right place to close the bracket by yourself, removing friction.

1 Like

Note that rust-analyzer already implements IDE-side mitigation:

ide

1 Like

Does it work for macros?

Yes, as depicted in the gif :wink:

You didn't really automatically close a macro after a long argument. And the idea is for it to be refactor-friendly, where you can just make point edits to add or remove a macro or type without looking for the closing brace. IDEs are good for writing code from scratch and maybe search-replace, but not much else.

Sorry, I feel like my original message came across as argumentative, which was not the intention. Rather, I want for folks to see the feature and realize that they can rely on it in their workflow, if they want to, today, as it might be non-obvious (for example, it took me several years to realize that it's painful for me to type <>, and that an IDE can do this instead).

In the similar spirit, extend selection feature can be used for pointed refactorings:

  • wrapping subexpression (not necessary a full expresion ) into a macro/funcion call:

And, for dbg! specifically (which is one of the most common wrapping constructs), there's a pair of postifx completion and assists:

I explicitely do not claim that fixing "language problems" with IDE is a good idea, but I do want to ensure that people are aware of the existing capabilities of tools at their disposal.

5 Likes

This is cool, I didn't know some of these features. But do you have an opinion on the proposal itself?

I don't really know how to weight language design trade offs, so I wouldn't necessary trust my own option.

That said, in this case it seems like the feature is optimizing for the benefit of the writer at the expense of the reader. Usually this is considered to be a wrong choice of priorities. For this reason (and for general reason that adding a language feature has learning, implementation and (present and future) feature interaction cost), I would say I like the language without this feature more.

4 Likes

I don't think this feature makes code less readable. On contrary, it cleans up unnecessary single-item brackets.

Here's a quick question: is what is bracketed by the macro unambiguous without knowing the macro contents?

Because Rust should be possible to parse without the parse being decided by something that parsed earlier (or later!). (In more formal terms, it should be context-free.)

4 Likes

It is not. For the macro feature, without some kind of hint about the type of the designator, it would not be context-free for all designators.

It should be fine if we limit designators to ty or expr, though.

Keep in mind that having a degree of redundancy in the grammar helps IDEs and the compiler get its bearing when encountering malformed input. If you have the following code

let x = 42
let y = 0

the compiler can figure out that you're missing ;s because of this redundancy and will tell you so. That redundancy also helps human readers as well. Removing all redundancy from the grammar by removing sigils can make it nicer to write but the consequences of doing that can be far reaching and be a net negative to the real world user experience of writing code. This is similar to a proposal that props up every now and then of making ; optional, then it is much harder to figure out intent when we encounter

x = 42
y = 0

where x and y are not previously defined. Did you mean let x = 42? Did you mean x = 42.y and you're missing the dot? It's not necessarily straight forward. Having visible delimiters helps not only the compiler but humans. Having different syntaxes for the same concept/feature makes the learnability of the language suffer.

The language also has a design constraint of having a regular grammar, not because rustc can't have more advanced parsing (in fact rustc supports a superset of Rust's grammar, in order to provide accurate suggestions in some occasions), but because we want to make life easier for third-party tools that might want to implement their own parsing.

The idea of delimiterless macros intrigues me and could be useful, but I think would fit with the language in a more cohesive way if it was tied to the not-yet-RFC postfix macros: this.expr::<could<be, anything>>().postfix!, as the expression that is being operated on becomes much clearer (for both humans and rustc).

10 Likes

This is cool, but could it be extended for types as well as expressions? What about a prefix notation?

Making the brackets optional is ambiguous:

let x: foo!(i32, bool); // does the macro accept two arguments or a tuple?
let y: bar![i32];       // does the macro accept `i32` or `[i32]`?

This could be worked around, but it would probably cause confusion.

Also, macros in type position aren't that useful, since we have type aliases.

I think you need a pair of parentheses after the macro (it is my understanding that postfix macros should accept additional arguments).

3 Likes

That's like saying macros in expression position aren't that useful, since we have functions. They serve different purposes.

I don't think that the ability to write Vec i32 vs Vec<i32> is at all worth the burden of extending the core language (and needing to force more work on users trying to parse Rust code, primarily macro authors and other consumers of Rust ASTs).

Furthermore, it's at best a subjective improvement; for one, I find delimiter-supported arguments more readable, because what goes into a macro/generic is clear without any more thinking needed.

In general, I find pure syntax aliases to be worth only in a very limited number of cases, and this one does not seem to provide a significant improvement over the state of the art.

2 Likes

Yes, there are possible use cases for macros in type position, but I personally have never used it or seen it in any Rust codebase. But other people might have a different experience, I don't know.

I agree that omitting angle brackets in types is probably a bad idea. It would break a lot of macros, and probably get confusing with const generics.

1 Like

frunk uses type macros to declare the type of hlists and coproducts,which can be of any length.

My structural crate uses the FP macro to get the type of a field path.The type-level representation of the strings in the field path types is unstable,and likely won't be stable until const generics (as in strings) are stabilized.

(Linking to the FP macro docs made me realize that I should improve the docs for it)

1 Like