Make macro syntax similar to function

Rust macro

macro_rules! <macro name> {
    ( arguments ) => {
    {
         Code block
    };
}

make it like:

macro <macro name> (argument)->return type or output type {
      Code block
}

is there any design reason why syntax of macro in rust is design such a way

macro_rules! <macro name> {
    ( arguments ) => {
    {
         Code block
    };
}
```  like flexibility or any other reason
or 
is there any problem if we design rust macro similar to function like syntax
please share your knowledge about rust macro, how it work internally and what make it different from other language macro type
1 Like
6 Likes

First of all, not sure what return type you expect to be there. Macros don't have access to the type system, as they are expanded long before type checking even begins. Fuethermore, not all macros expand to expressions; often, macros are used for generating items such as function definitions and trait impls.

Second, there are reasons why you need to surround the macro body with additional curly braces. First, macros often accept syntax which is already heavy on parenthesization. Therefore, it would be confusing to make parentheses and braces optional, because it would then be syntactically ambiguous (or at least unclear to the reader) whether the outermost parentheses/braces are part of the macro definition or the expansion.

Second, macros are pattern-matched against the input; they can have multiple "overloads" ans each such overload has its own expansion. Therefore, explicit separators/grouping are needed in order for the compiler to be able to tell apart successive macro bodies. (A simple trailing comma or semicolon wouldn't suffice because any number of those can be generated in any position by a macro body, whereas parens/braces must be correctly nested and balanced.)

2 Likes

IIRC this is based on Kohlbecker & Wand's POPL '87 paper:

https://dl.acm.org/doi/abs/10.1145/41625.41632

I do think it would be nice to have an "easy mode" syntax for simple function-like macros that just take expressions and produce expressions, especially for local (non-exported) helpers.

For example, I could imagine something like

macro fn divrem(a, b) => (a / b, a % b);

as a simple way to DRY some code locally, without needing to go through the effort of specifying argument and return types.

(That's not a great example, because I could just use num's method, but hopefully it gets the gist across.)

2 Likes

Actually, it does make some sense to have a macro explicitly declare the syntax class it expects to expand to, which makes it easier to "parse-check" the body of the macro, although I don't know how useful it would be for macro applications, especially if the actual syntax class of a macro depends on which variant is invoked. It's true that macros don't have access to the type system, but they do still have a sort of "AST-hygiene" in that they need to fit into the grammar somehow and it might be useful to have stricter typechecking around which nonterminal is getting injected (which already exists in macro arguments, since you have to write $e:expr vs $e:tt, etc).

3 Likes

Ooh, I really like that. For use cases like this, the current declarative macros require so much accidental complexity...

1 Like

Is this supposed to be a special kind of macro, or a special kind of function? If it's a macro, I'd assume that it is invoked as divrem!(...), and that a and b are evaluated twice. But if it's more function-like, I'd assume that it's invoked as divrem(...), and a and b are only evaluated once. In either way, I guess that it doesn't support recursion?

Note that there will be a shorter syntax for declarative macros with the macro keyword, so the above can be written as

macro divrem($a:expr, $b:expr) {
    ($a / $b, $a % $b)
}

or, if a and b should be evaluated only once:

macro divrem($a:expr, $b:expr) {{
    let (a, b) = ($a, $b);
    (a / b, a % b)
}}
1 Like

I was under the impression that rust macros already only evaluate their inputs once. Is that not true?

No. For example:

macro_rules! foo {
    ($e:expr) => { $e + $e }
}

foo!(17f64.sqrt());

This expands to 17f64.sqrt() + 17f64.sqrt(), so the sqrt method is called twice. This is not the case with a function such as

#[inline]
fn foo(e: f64) -> f64 {
    e + e
}

foo(17f64.sqrt());

Since sqrt() has no side effects, LLVM might be smart enough to optimize it to the same code, but that's not possible when the expression has side effects.

1 Like

I was thinking of it as something in-between. That it would operate only on values and couldn't affect the parent's control flow (like a function, and thus not need the !) but would be compiled as part of the caller (like a macro) so that all the types would be inferred and would have more borrow-check flexibility.

But this is very much only a sketch of an idea. I definitely haven't thought it through in detail, so I don't have all the answers and there may well be hidden traps.

1 Like

I believe @matklad has suggested this before, and it has analogues to Kotlin's inline fun. It is basically halfway between macros and functions: it has the API semantics of a function, but the code is evaluated in the caller's scope.

In Kotlin this is mainly used for TCP-preserving closures, IIRC, but it's been a good while since I wrote any Kotlin.

1 Like

It feels that what you want to do is to create the equivalent of C++ unconstrained template functions.

// C++
auto divrem(auto a, auto b) -> decltype(auto) {
    return {a / b, a % b};
}
// Pseudo Rust where the keywords `type` and `trait` means
// "interfered by the compiler"
fn divrem<A: trait, B: trait>(a: A, b: B) -> type {
    (a / b, a % b)
}

I’m not advocating for such syntax (it’s way too heavy), but I found the parallel with C++ interesting.

Sortof, but maybe not exactly. There are a few things that jump to mind that could plausibly work better with the macro-style compilation strategy instead of the decltype strategy: initialization checking & drop flags, subset borrowing (&mut foo.a and &mut foo.b), contextual inference, and general lifetimes.

To elaborate on one of those as an example, in C++ one can move from a mutable reference, because it's expected that doing so leave the object in a safely-destructible but otherwise useless state. But in Rust we can't move out of arbitrary &muts, so turning a macro-like thing into a "real" function -- even if fully inferred -- limits what can be done vs the macro strategy. So anything that wants to do x = something_moving_the_argument(x); can only go in a macro fn if that doesn't translate to something where the place x weakens to a &mut x.

(Which I guess means I was wrong in my previous post when I said it wouldn't need !. It probably would, if it can do things like passing places as places, rather than as values.)

1 Like

@petertodd also suggested roughly the same thing in this thread, which has some good discussion:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.