In this code, there is a !
after the println
:
fn main() {
println!("Hello, world!");
}
In most languages I have seen, the print operation is a function. Why is it a macro in Rust?
In this code, there is a !
after the println
:
fn main() {
println!("Hello, world!");
}
In most languages I have seen, the print operation is a function. Why is it a macro in Rust?
By being a procedural macro, println!()
gains the ability to:
Automatically reference its arguments. For example this is valid:
let x = "x".to_string();
println!("{}", x);
println!("{}", x); // Works even though you might expect `x` to have been moved on the previous line.
Accept an arbitrary number of arguments.
Validate, at compile time, that the format string placeholders and arguments match up. This is a common source of bugs with C's printf()
.
None of those are possible with plain functions or methods.
See also:
>None of those are possible with plain functions or methods
oh wow, no variadic arguments in Rust?? –
Tad Well, lets pretend we made those functions for a moment.
fn println<T: Debug>(format: &str, args: &[T]) {}
We would take in some format string and arguments to pass to format to it. So if we did
println("hello {:?} is your value", &[3]);
The code for println would search for and replace the {:?}
with the debug representation for 3
.
That's con 1 of doing these as functions - that string replacement needs to be done at runtime. If you have a macro you could imagine it essentially being the same as
print("hello ");
print("3");
println(" is your value);
But when its a function there needs to be runtime scanning and splitting of the string.
In general rust likes to avoid unneeded performance hits so this is a bummer.
Next is that T
in the function version.
fn println<T: Debug>(format: &str, args: &[T]) {}
What this signature I made up says is that it expects an slice of things that implement Debug. But it also means that it expects all elements in the slice to be the same type, so this
println("Hello {:?}, {:?}", &[99, "red balloons"]);
wouldn't work because u32
and &'static str
aren't the same T
and therefore could be different sizes on the stack. To get that to work you'd need to do something like boxing each element and doing dynamic dispatch.
println("Hello {:?}, {:?}", &[Box::new(99), Box::new("red balloons")]);
This way you could have every element be Box<dyn Debug>
, but you now have even more unneeded performance hits and the usage is starting to look kinda gnarly.
Then there is the requirement that they want to support printing both Debug and Display implementations.
println!("{}, {:?}", 10, 15);
and at this point there isn't a way to express this as a normal rust function.
There are more motivating reasons i'm sure, but this is just a sampling.
For (fun?) lets compare this to what happens in Java in similar circumstances.
In Java everything is, or can be, heap allocated. Everything also "inherits" a toString
method from Object
, meaning you can get a string representation for anything in your program using dynamic dispatch.
So when you use String.format
, you get something similar to what is above for println.
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
Object...
is just special syntax for accepting an array as a second argument at runtime that the Java compiler will let you write without the array explicitly there with {}
s.
The big difference is that, unlike rust where different types have different sizes, things in Java are always* behind pointers. Therefore you don't need to know T
ahead of time to make the bytecode/machine code to do this.
String.format("Hello %s, %s", 99, "red baloons");
which is doing much the same mechanically as this (ignoring JIT)
println("Hello {:?}, {:?}", &[Box::new(99), Box::new("red balloons")]);
So rust's problem is, how do you provide ergonomics at least as good as or greater than the Java version - which is what many are used to - without incurring unneeded heap allocations or dynamic dispatch. Macros give a mechanism for that solution.
(Java can also solve things like the Debug/Display issue since you can check at runtime for implemented interfaces, but that's not core to the reasoning here)
Add on the fact that using a macro instead of a function that takes a string and array means you can provide compile time errors for mismatched or missing arguments, and its a pretty solid design choice.
© 2022 - 2024 — McMap. All rights reserved.