Is it just me, or does Rust feel much more bare-bones than other languages? I just started learning it recently and this is the one thing that stood out to me, much more so than the memory management business. A lot of things that would normally be part of the language has to be achieved through meta-programming in Rust.
Is this a deliberate design choice? What do we gain from this setup?
Edits:
- Somehow, this question is being interpreted as a complaint. It’s not a complaint. As a user, I don’t care how the language is designed as long as it has a good user experience, but the curious part of my mind always wants to know why things are the way they are. Maybe another way to phrase my question: Is this decision to rely more on meta-programming responsible for some of the good UX we get in Rust? And if so, how?
- I’m using meta-programming to mean code that generates code in the original language. So if I’m programming in Rust, that would be code that generate more Rust code. This excludes compilation where Rust gets converted into assembly or any other intermediate representation.
Not sure if you’re talking about the language, or the core/alloc/std libraries, or both/something in-between?
Can you provide specific examples, an which specific languages are you comparing against?
Rust tries to move language functionality to libraries where possible. Instead of adding high-level magic to the language, Rust prefers to add a low-level feature that can be used to build higher-level features. For example, instead of built-in nullable types, it has enums with data, which were used to make Option. This way hopefully you can do more things with fewer language features. Functionality of higher-level features can be explained by lower-level ones (knowing how things are implemented is important for systems programming), and you can reimplement them if you need (e.g. Rust in the Linux kernel uses its own smart pointers instead of those from the standard library).
Rust tries to keep the standard library small, and move unnecessary code into crates-io crates. The problem with stdlib is that there is only a single version shared by all programs, so it has to stay backwards-compatible forever. Long term stdlib accumulates outdated functionality and deprecated APIs, which can’t be fixed. Crates.io crates support versioning, so they can evolve and iterate without breaking anyone.
Another reason is that Rust supports low-level programming, including embedded. This means that the language itself can’t depend on any fat runtime, and doesn’t even perform heap allocations.
I’m not talking about what features are in the standard libraries vs third party libraries. I mean meta-programming as in the stuff that generates Rust code. Take console printing for example, we use a macro
println!in Rust. Other languages provide an actual function (e.g.printfin C,System.out.printlnin Java,printin Python, etc). The code for my first project is also full of things like#[derive(Debug,Default,Eq,PartialEq)]to get features that I normally achieve through regular code in other languages. These things are still in the Rust standard library as I understand it.printfuses macros in its implementation.int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = __vfprintf_internal (stdout, format, arg, 0); va_end (arg); return done; }^ This is from glibc. Do you know what
va_startandva_endare?to get features that I normally achieve through regular code in other languages.
Derives expand to “regular code”. You can run
cargo expandto see it. And I’m not sure how that’s an indication of “bare bone”-ness in any case.Such derives are actually using a cool trick, which is the fact that proc macros and traits have separate namespaces. so
#[derive(Debug)]is using the proc macro namedDebugwhich happens to generate “regular code” that implements theDebugtrait. The proc macro namedDebugand implemented traitDebugdon’t point to the same thing, and don’t have to match name-wise.^ This is from glibc. Do you know what
va_startandva_endare?Does anyone? 🙃
For functions that want to accept variadic arguments in C/Cpp
Yup; I was referring to their implementation being dark magic, depending on calling convention, argument type, argument order and possibly other properties specific to the processor’s instruction set…
“If you think you understand
va_*, you don’t.”Yeah, didn’t catch your sarcasm there :D
I was just referring to the fact that they are macros.
Using a function is strictly worse than figuring out the formatting at compile time (something Zig also does).
The derives are just shortcuts. You can write everything out long-hand like you would in C++ or Python too if you really want.
Honestly both of these complaints are essentially “why does Rust use macros to make writing code better/easier?”.
Regarding the derive macros, there are a few reasons these are required.
- Rust does not have a language runtime (like Java). So certain features that would normally require reflection instead require an opt-in trait implementation. This is part of Rust’s “zero cost abstractions” philosophy. You don’t pay for code you don’t need.
- You get the benefit of being able to customize the behavior of those core traits. Rather than doing something simple (and wrong) for every type, like a byte-for-byte equality check, you get to define the behavior that is appropriate for a given type.
- The derive macros are just a convenience. You are free to use “regular code” to implement those traits instead.
Can you give some examples?
What languages provide out of the box varies a lot. Rust provides a ton of things compared to say C.
What are you talking about specifically? I’ve written C and C++ code and it’s terrible. UTF strings were an absolute pain, you needed Boost for the simplest things (though many things of boost have been assimilated in the standard), there are a thousand different ways to do simple things like iterating through iterables (again, things have changed in the standard), there’s no default dependency management, and so much more.
Rust comes with dependency management, a way to write unit tests and integrations, generate docs, toggle features, has standard iterables, string handling, async, compiler targets, and a lot more things C and C++ could only dream of having.
Don’t even get me started on zig which doesn’t have its own friggin string class / struct whatever.
So again, I ask you, what are you referring to? Please provide examples.
C++ was my first programming language. I remember the nightmare of dealing with dependencies and avoiding boost because it felt wrong to need a third part library for basic features. The toolchain for Rust is very nice (not just compared to C++, but all other languages I’ve worked with) and has so far been a huge joy to work with. The language itself too. I’m just curious about why the language likes to expose more of its features through meta-programming rather than directly in the language itself. Things like
println!andformat!being macros instead of functions, or needing a bunch of#[derive(Debug,Default,Eq,PartialEq)]everywhere for things that other language provide through regular code.Source needed because the Rust library is bigger than the STL, no one cares about print being a macro, and derivation being an issue only applies when you compare Rust to scripting languages.
Rust is a contender to C and C++, not Visual Basic that can do everything poorly.
A lot of things that would normally be part of the language
Give examples because println is not convincing. Why would you have println on an embedded system that has no display?
I’m not saying that there’s a problem with doing things one way or another. Rather, I’m asking whether there’s a problem with doing things differently that then led to this design decision to be made with Rust. I want to better understand how this language came to be.
I see what you mean. OK, I haven’t read the rust language specs, RFCs or whatever, so this is my guess why it was done that way.
Regarding derive: Rust doesn’t have inheritance but it does have traits / interfaces. This is both an advantage and a disadvantage. For environment with constrained resources, having structs that dont implement a bunch of things you don’t need is an advantage. The downside is that in other cases, you need to know what you want.
For example not every class needs to be comparable or know how to print itself to debug output. But if you do want that, you need to know to implement the right trait. Implementing the same trait the same way all the time however is just boilerplate, hence, the derive macro.
At the same time proc-macros like derive ate extremely powerful since they ingest the syntax tree and spit one back out. It allows you to do crazy stuff at compile time with all the IDE goodness of showing docs, finding symbols, and type checking. IDEs can predictably expand macros. I have yet to find an IDE besides CLion that does the same with C/C++ code.
Process macros in rust can do things like consume files (statically as in inserted by the compiler as opposed to dynamically I’m the function browsing the filesystem), which allows reading and interpreting another language, like python or perl, to generate symbols for those other languages. It is thus possible for example to write a python module completely in Rust. With work, it theoretically is possible to also extend python classes (or symbols from other languages) purely in rust.
As for println! and format!, those macros are in the STL. They don’t make it heavier. I think they are macros because rust doesn’t (or didn’t) support variadic arguments.
Try Forth if you want a bare bones language.
You can use macros to generate code, both declaratively and programmatically. Doee this help?
why is common lisp so barebones? it’s literally nothing but macros! WHERE’S THE SYNTAX




