Let me offer yet a different perspective on this RFC, with some prior art.
I prefer to talk about this feature as "transparent functions" rather than "macro functions", even though my preferred syntax is macro fn
(mostly because macro
is an already reserved keyword, the semantics are vaguely related, and I don't know of any better word). Calling them "macro functions" is a bit confusing, because they are much closer in their semantics to functions than to macros. For example, if the signature isn't generic, then a transparent function is basically the same as an ordinary function.
I call them "transparent functions" because the primary difference from ordinary functions is their signature transparency. Normally in Rust a function's signature is a hard boundary for any kind of compiler analysis (unfortunate exception: impt Trait
in return position and its interaction with auto traits). This gives powerful stability guarantees and is particularly invaluable for public APIs. However, this also brings complications, because the local type inference is significantly more powerful than global one, and some things are impossible to imitate at the function level (e.g. disjoint borrows).
Transparent functions opt out of opaque signatures, allowing type inference, borrow checker and possibly other analyses to look into the body of the function. This leads to a result which is reminiscent of duck typing or C++ templates, but it is neither. Instead the closest analogue is global type inference in languages such as OCaml and Haskell. In those languages there is no difference between a global function and a local closure, unlike Rust, where those have very different syntax and capabilities. You still need to declare the (parametric) signatures of all functions exported from a module, but you don't need to declare all signatures within a module.
I'll stress it again: it's the same type inference, the same syntax and the same functions. We just allow the compiler to deduce more details about our code, but if there is ambiguity, compilation will fail, as usual. This is in stark contrast with C++ templates, where the compiler will happily pick wrong instantiations out of a very hard to control list of functions, and where template resolution is (mostly by design) a Turing-complete process. Hindler-Milner type inference is guaranteed to result in a single valid type in finite (but possibly exponential) time, or fail to typecheck.
Now, it's true that global type inference can result in a brittle code at large scale, where a change in the body of some function causes a cascade of inference changes, breaking something unexpected in a different place. For this reason explicit documentation of type signatures is still a good practice in those languages. However, it works great at small scale (e.g. within a single, even large, module), and one can always use more specific type signature if a globally inferred one causes problems.
While Rust's type system is way more complicated than pure HM, and it's quite different from both OCaml and Haskell, I believe the same principles apply. The defaults of Rust are correct: for ecosystem stability, all types must be explicit and type inference should be only local. But I believe that relaxing that requirement and enabling global inference in specific explicitly annotated cases would be beneficial. It would facilitate simple code reuse, particularly in the cases which run afoul of the borrow checker, and reduce boilerplate in cases where explicit trait bounds do more harm than good.