Hacker News new | ask | show | jobs
by cuddlybacon 2164 days ago
As someone who hasn't used Rust, I am curious about why Rust has macros.

I use C++ at work, which admittedly isn't the language I use most, and macros are used quite a bit in the code base. I find they just make the code harder to read, reason about, debug, and sometimes even write. I don't see them really living up to their claimed value.

Is there something different about Rust's macros that make them better?

5 comments

There is almost no intersection between the kind of things that can be done with the C++ macro system and the kind of things that can be done with the Rust macro system. They are not related. You can see them as another feature that is not available from C++.
You can get quite close to the use case for Rust macros by considering C++ template-based metaprogramming. Of course the biggest difference is that Rust macros have been designed from first principles, not as a clunky afterthought.
Indeed, Rust macros are a descendant of Scheme macros, not of C macros.
>I use C++ at work, ..., and macros are used quite a bit in the code base.

Ouch. Modern C++ has alternatives to C macros. Is it an old code base, or is it just written in the C++98 style?

Today people will typically use constexpr instead of #define. While macros can possibly do some funky things constexpr can't, you'd be hard pressed to find those things. (C++ supports constexpr if, constexpr functions, constexpr lambdas, math, and so on.)

If you have the time and effort, it may be worthwhile to slowly start modernizing the code base, bit by bit.

The advantage of modern day C++ is it catches errors at compile time that older versions did not and would crash while the software was running. You might improve the stability of the code base if you help out.

Writing macros (in any language) is a way of creating an abstraction. Creating abstractions is a way of automating the job of programming, making it ideally more efficient and less error prone. This is why people usually prefer java to basic. Marcos are a way of creating abstractions that are particular suited to be "concreted" by the compiler, making them a ideal match for programming languages that seek to be "close to the metal" like C, C++ and rust.

C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them. IMHO the real macros of the C++ language are the templates and constexpr, althou they are limited in other ways. E.g. its hard to extend the syntax using them or do certain things like making the calling function return. They grow ever more powerful, with their own type system (concepts) and things like std::embed and static refection so they finally feel like a real language, alsbei a clumsy, pure functional language that feels alien a C++ programmer without exposure to haskell.

Rust macros are actually meant to feel like Rust, not some ad-hoc bolted on language.

> C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them.

I think this may be why I'm having a hard time appreciating them. Probably half the macros I see could just be a function call. The majority of those that don't are hiding a conditional return or goto, which I find to be a net negative.

I'll probably have to use a language with good macros before I can appreciate them.

> Probably half the macros I see could just be a function call.

Yes, that particular breed of C macros would likely manifest in Rust as people just defining a new function. In Rust, you tend to see macros in places where "just make a new function" doesn't suffice for whatever reason; for example, maybe you need to define a dozen different structs that only differ by the type of one field, so instead of actually defining the struct a dozen times, you could just define the struct inside the macro and then `define_my_struct!(u8); define_my_struct!(u16);` and so on.

You also can't use Rust macros to "redefine" other unrelated pieces of code, so that's one less thing to worry about.

Your define_my_struct! actually seems useful. Thanks for that example.
The first thing I would say is that regular inline macros are hygienic and type safe, just like the rest of the code you write. That way they're not just text expansion but smart code expansion.

When I say hygienic, I mean that when a macro uses a variable that isn't in the parameters or isn't static, it will be a compile error as it cannot know the scope. Any variables defined inside are scoped. Only parameters parses in can be referenced from outside the scope of the macro

They're also notated different to regular functions and they can't appear anywhere so it's obvious when you see a macro that it will expand into some code that won't break any of the other code in your function.

Of course people can write really terrible macros but typically macros serve a single very simple task and should be well documented and in my experience they often are.

As for procedural macros. They are just ast in, ast out functions, but written as a library in regular rust rather than some language thrown on top. That makes it easy to reason about the code and make it safe. If you write them well, they can be very good at reporting errors in usage to users.

The std lib also provides some very straightforward yet useful macros to act as inspiration. String formatting is an inline macro. vec is a macro to quickly define Vectors. Derive debug, partial equality, default values are all procedural macros and they are very straightforward yet tedious tasks to do on your own all the time.

Given the macros people publish, I would say they've done a good job at securing them as a useful feature. I've not seen many instances in actual code bases of messy macros. For examples of great macros, see serde[0], clap[1], inline-python[2]

[0]: https://github.com/serde-rs/serde [1]: https://github.com/clap-rs/clap [2]: https://docs.rs/inline-python/0.5.3/inline_python/

I’m not a rust programmer, reading your explanation here it’s not anymore clear why some macros exist (println for instance). They very much look similar to functions in most of the cases I’ve seen (again from the perspective of trying to find a reason besides safety to learn rust) so why exactly do these exist? They are a great source of confusion for those of us with C/C++ experience (or at least me) which is the target audience.
println is a macro in Rust because 1) the language currently lacks support for variadic functions and 2) being a macro allows the format string to be parsed at compile time.
Long-term, with const generics, and VG (or a macro that creates a (a, (b, (c, ()))) nesting like the hlist crate), we could maybe replace format_args! and friends.

However, there is one more thing a function can't do: borrowing arguments. Formatting never moves arguments because the format_args! macro generates references to them, which it then creates std::fmt::Argument out of.

Most languages end up needing a way to step outside that language, and if you don't have macros you end up using something even worse. Rust in particular lacks "do notation" or proper support for higher-kinded types, so it needs to use macros as an ad-hoc replacement for things like sequencing async operations or proper propagation of errors. To be honest I'm surprised they didn't find a way to make a web framework in plain rust though.