I guess I’d rather have the feature for when it’s truly necessary than exclude it from the language entirely, and just hope that people use it with forethought.
I guess I’d rather have the feature for when it’s truly necessary than exclude it from the language entirely, and just hope that people use it with forethought.
Maybe macros make sense as a pragmatic way to allow prototyping of future language features. But language designers can, and IMO should, work towards making them unnecessary in the production-code ecosystem, because the disadvantages for readability and tooling are very real. And that's only going to get worse as more advanced tooling (IDEs, profilers etc.) becomes available and people rely more heavily on those tools to be deeply integrated with the language, because those tools are never going to be able to understand ad-hoc macros.
What frunk does at runtime (well, its proc macros are still compile-time, but the code that makes use of the metadata is runtime), syn does at compile-time. I can understand that it's nice to have a simpler model where everything happens at runtime, but the things that proc macros can do include things that matter to the typesystem. For example, some of the macros I've written generate new types based on the input tts. You can't do that at runtime.
>And that's only going to get worse as more advanced tooling (IDEs, profilers etc.) becomes available and people rely more heavily on those tools to be deeply integrated with the language, because those tools are never going to be able to understand ad-hoc macros.
The mainstream tools (rls and rust-analyzer) get their information from the compiler, and thus have access to the expansion of the macro rather than just the macro invocation.
Again, though, that's something you want to be able to do in a standard first-class way in the language, rather than needing a custom macro. Depending on the use case you either want higher-kinded types (for transformations like producing a "patch" version for a given record, where all members are (recursively) optional) or dependent types (for transformations where you really just want to do a bunch of specific type->type cases). It's really not rocket science - Scala has been doing this stuff for years already.
> The mainstream tools (rls and rust-analyzer) get their information from the compiler, and thus have access to the expansion of the macro rather than just the macro invocation.
Sure, and that helps, but the tool can't possibly know what the full relation between input and output is (because by definition the macro is an arbitrary function), so it's never going to be as reliable as first-class code.
The macro can associate output tokens with the spans of input tokens, either by reusing the input tokens (`#[derive(Foo)] struct S;` -> `impl Foo for S`, the `S` is reused), or using the macro API which allows specifying an arbitrary span for new tokens. This is required both for ident hygiene and for associating errors with the source code, thus it is a core part of the macro API. And since the compiler has this information, tools have it too.
That means if you have
#[foo]
struct S; // (1)
let s = S; // (2)
and use an IDE to refactor-rename `S` to `S2` at the line marked (2), the IDE has enough information to know that it must also rename the `S` at the line marked (1), even though it's a macro input. This applies equally well to function-style proc macros `foo! { S }`Agreed. But in order to receive that bug report, you need a macro system. This is partially because...
> Rust is crying out for a way to treat structs generically (i.e. what frunk does with LabelledGeneric)
frunk is very cool, and I'm glad it exists, but it gets about 100 downloads a week. The matches macro that we uplifted to the standard library in this release is getting around 20,000.
> the disadvantages for readability and tooling are very real
Interestingly enough, just this week rust-analyzer gained the ability to do code completion within macro invocations.
Indeed, because library authors find it easier to write their own macro to solve the problem. And maybe part of that is that frunk is too hard, but I do think it's also the case that macros are too easy - or rather, that the ease of adding them is not proportionate to the ease of maintaining codebases that use them. http://www.haskellforall.com/2016/04/worst-practices-should-...
One of the first things I wrote was a procedural macro. Yeah, I like to learn things the hard way, but I'm glad I did! Once I got past the confusing split between proc_macro::TokenStream and proc_macro2::TokenStream, I found that using "syn" and "quote!" is really quite enjoyable.
I'd rather have a small simple concise 1-liner, than being forced to write 20 lines of code otherwise.
In Go you have `if err != nil` on every other line it seems and that makes things hard to read. The `try!` macro which later became the `?` makes things much easier to read IMO. Especially when you have to drill into something nested.