Hacker News new | ask | show | jobs
by CalChris 3400 days ago
As I've said/posted this elsewhere, the Rust macro package is close to unusable. It makes easy stuff difficult and it doesn't exactly help with difficult stuff.

It would be interesting to compare the number of macros defined in the crates corpus divided by total line count and compare that with other languages. I do not think that I am alone in not using it. Yes, I use macros; I just don't program macros.

Obviously, Java has shown that you can survive without a macro pre-processor. That was even a point Gosling+Co made in a white paper I read way back in the day. But I do believe that if you are going to have a macro processor, it should be an expedient. Rust's macro processor is not expedient. It is its own impediment.

I'm used to using macros. I use them in C and I use them in assembly. These are both low level languages which Rust claims to be. Not being able to use Rust's macros in the style to which I've become accustomed is infuriating.

4 comments

Macros are being largely re-done, see http://words.steveklabnik.com/an-overview-of-macros-in-rust for an overview.

Honestly, I very rarely use macros and have written two in my years of Rust. You almost never need them, or at least, that's my experience.

I guess it depends on what you work on. Both of my primary Rust projects (a .NET metadata parser & a Game Boy emulator) are heavily dependent on macro usage, and they implement multiple new macros. Both do lots of binary parsing, so I use bitflags, enum_primitive and bitfield all the time. For instance, I use this [1] two-macro monstrosity to parse tagged unions from the CLR metadata.

[1] https://github.com/paavohuhtala/clri/blob/b8a9057397ef95c0de...

Yup, I do think it varies this way.
And even in Clojure, where macros are very easy to use -- I write them very infrequently. They're seductive, but usually a bad idea.

  #define M_PI        3.14159265358979323846264338327950288
3.141592653589793115997963468544185161590576171875 is the exact decimal representation for the 64-bit IEEE-754 number that's closest to pi (viz. 0x400921FB54442D18).

Any time you see a decimal floating-point constant with a nonzero fractional part that doesn't end in '5', you're looking at a bug.

EDIT: As long as this grizzled old Fortran programmer is giving out free advice, I'll add two more items every programmer should know about binary floating-point:

a) Every binary floating-point number can be represented exactly in decimal notation if you use enough digits.

b) Those decimal values are the only ones that can be exactly converted to binary; all of the rest require rounding.

> Any time you see a decimal floating-point constant with a nonzero fractional part that doesn't end in '5', you're looking at a bug.

That's just silly. If you're writing some famous mathematical constant, the digits should match that constant, and not the requirements of the machine. (Except for the last one being rounded off.) Suppose we had a floating-point machine that gave us maximum 4 digits of decimal precision. I wouldn't define the PI constant as 3.145. That would just look like a typo to people who have PI memorized to half a dozen digits or more. I'd make it 3.14159 (or more) and let the darn compiler find the nearest approximation on the floating-point axis.

Any exact decimal representation of a specific binary floating-point number that's finite and not an integer must end in the digit '5' (perhaps with trailing zeroes). This is because its fractional part is (the sum of) a set of powers of two with negative exponents, and their exact decimal representations (0.5, 0.25, 0.125, &c.) all end in '5' (proof by induction is obvious and left to the reader).
Any time you see a decimal floating-point constant with a nonzero fractional part that doesn't end in '5', you're looking at a bug.

depends on the language. for example here it is in go:

    Pi  = 3.14159265358979323846264338327950288419716939937510582097494459 // http://oeis.org/A000796
What the person above you is saying, I think, is to remember that computers usually work in base 2. This applies to IEEE floating points, where the mantissa is in base 2; when you represent fractions in base two, they're powers of two: 1/2 (.5), 1/4 (.25), 1/8 (.125), etc. What he's asserting, I think, is that any power-of-two fraction, or any combination of those (in binary), result in a number ending in 5 when represented in decimal. Anything else is going to be rounded to the nearest representable number (that ends in 5).

So, go might have that value in its source, but it's getting rounded to something that would, if represented in decimal, end in 5.

In Go, floating-point constants may have very high precision, so that arithmetic involving them is more accurate. The constants defined in the math package are given with many more digits than are available in a float64.

Having so many digits available means that calculations like Pi/2 or other more intricate evaluations can carry more precision until the result is assigned, making calculations involving constants easier to write without losing precision. It also means that there is no occasion in which the floating-point corner cases like infinities, soft underflows, and NaNs arise in constant expressions.

There's an argument to be made the other way too. If you're using an unusual default rounding direction and you care which direction your PI constant is rounded, you might prefer it if PI rounded in the same rounding direction as the rest of your floating point math. In that case you'd want a constant that is equivalent to PI under all rounding modes.
The code below works for me

#define PI1 3.141592653589793115997963468544185161590576171875

double pi1 = PI1;

#define PI2 3.14159265358979323846264338327950288

double pi2 = PI2;

assert(pi1 == pi2);

(edit: or even 3.14159265358979323846)

Your PI2 rounds to PI1 under the rounding mode used at compilation time. Print it out with "%50.48f" (or FORMAT(F50.48)) and you'll see PI1. But PI1 is independent of rounding mode.
Sure. I thought you were implying more than that. When you called it a bug I thought you were implying that the use of one over another would alter the output of a program. My bad.
I thought that style was considered bad form in modern C/C++ (esp. the later, with `constexpr`). What's wrong with

    const PI: f64 = 3.14159265358979323846264338327950288;

?
What machine can represent all those decimal digits so precisely that the ending decimal digits ...0288 are exactly right and not ...0287 ?

I'll accept without looking it up that the statement is correct syntax in some language (it doesn't look like C++ to me).

See above. The constant is (a) not pi, and (b) not an exact decimal representation of any binary floating-point number.
const M_PI: f64 = 3.14159265358979323846264338327950288;

You wouldn't use a macro for something like that in Rust.

I don't understand what this means. You'd use const in Rust for this, not a macro.
That was just a trivial example. Yes, you could use a const which would be difficult to share across files. Moreover, if you wanted to do something equally textual

  #define PASS_VERBOSE if (flag_verbose && first_pass) printf
you just might be able to but only with a completely different tool.

A macro processor is not of the language; it is above the language.

A "macro" in C is not the same thing as a "macro" in Rust. By your logic, Erlang processes aren't processes because they aren't kernel processes, or Go packages aren't packages because they don't use the Java package naming convention. Nobody owns the exclusive right to define the words we use.
> A macro processor is not of the language; it is above the language.

Then feel free to use the C preprocessor with Rust, it works just as well. :P Just like it does with Python, and Java, and...

The C preprocessor can only be used with Python as long as you don't do anything multi-line:

   #define whatever(param) \
   foo: \
      bar \
      baz

I made a preprocessor some 18 years ago that could be used with Python.

Wayback Machine:

https://web.archive.org/web/20000815202258/http://users.foot...

Better yet, use m4.
> which would be difficult to share across files.

It's path::to::wherever::PI. That's it. Just like any other item. If you want to use only PI in your code, you'd use 'use', like any other name.

Your second example is something better suited to a macro, it's true.

I _think_ what you're getting at here is that you only want text substitution? I think we will have to agree to disagree if that's true :)

Well, given that you have only written two macros in your years of Rust, I would strongly encourage you or the language ergonomics initiative to openly question why this is so. Clearly, Rust has a clever approach but I'm questioning whether it is in fact a usable approach. Too much solution for not enough problem.

Yes, I do like textual substitution. Guilty. This is a common old school low level paradigm. Still, the underlying language and its compiler exist below to enforce the rules on any atrocities I commit with my macros.

You want textual macros. Rust doesn't have this feature, it has a syntactic macro feature inspired by the Lisp family. Sorry.
> Obviously, Java has shown that you can survive without a macro pre-processor. That was even a point Gosling+Co made in a white paper I read way back in the day. But I do believe that if you are going to have a macro processor, it should be an expedient. Rust's macro processor is not expedient. It is its own impediment.

Annotation Processors (iirc Java 1.5) are clearly a form of a pre-processor. It's not macros / textual expansion, though.

Similarly C++ mostly gets along without macros since it contains a capable meta-programming system -- and I think this is the more important point here; for many tasks meta-programming is just a handy thing to have. Dynamic languages don't have that problem, since their runtime is their meta-programming system as well.

Another thing that has helped Java was the decision to use a JIT.

Most Java JITs are able to remove code if it is proven unreachable, which allows to use pure Java code for what would be #ifdef in C, with the caveat that all branches must compile.

You would think that AOT would be the right time to remove dead code.
Only if it can be fully determined at compile time.

For example a debug flag can depend on a command line parameter, but it will be used to initialize a constant variable.

So the code can be shipped with both versions, and the JIT will just remove the unused branches.

absolutely agree and I think this issue is much bigger than everything together what is described in that article. Solving macros syntax issue would solve a lot of issues with verbosity.
I really hope one day I can build a macro for ternary operator with ? and :.
Is using the if statement as a ternary that much worse?

    let x = if a { b } else { c };
Sure, it costs a few characters, but I appreciate the consistency and clarity.
Much agreed - I quite regularly write code like that, and it's very clear on reading the first thing after the `=` that you're doing something conditional. You can also embed complex expressions in there without confusion. The ternary syntax requires you to read the entire statement before you even know what kinds of expressions it contains, then backtrack to figure it out.
I would strictly call that an "if expression" rather than an "if statement". It is equivalent to ?:, even though it is also a bit more verbose.
Verbose.
The thing is, Rust's Option type means the primary use of the ternary in other languages - `foo ? foo : somedefault` - is entirely unnecessary in Rust. Other uses of it tend to benefit significantly from being more obvious about what's happening.

I think a ternary operator would be the first construct in Rust that prevents reading a statement from left to right.

No offence, but I hope you never can, because if you can, others can, and since Rust already uses '?' for something else, it's likely to just get confusing. Is the if syntax for ternary really that bad?
It's in different context. Reusing a keyword or an operator in different context happens all the time in languages.
Is it that different? Assuming you would use '?' and ':', presumable we might see both the following then:

    let foo = foo()?;
    let bar = bar() ? this() : that();
? already has different forms as of now. You can do foo()?; or foo()?.bar(). People don't seem to be confused.

expr ? expr : expr is just another form. The ternary form is so well known that I doubt people have problem recognizing it.

The usage of ? in foo()?.bar() is no different from its usage in foo()?; ...
Are those really 2 different forms? I was under the impression that these were the same:

    foo()?.bar()
    (foo()?).bar()