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:

  1. 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?
  2. 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.
  • kornel
    link
    fedilink
    English
    arrow-up
    17
    ·
    2 days ago

    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.

    • cx40OP
      link
      fedilink
      English
      arrow-up
      6
      ·
      2 days ago

      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. printf in C, System.out.println in Java, print in 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.

      • FizzyOrange
        link
        fedilink
        arrow-up
        3
        ·
        1 day ago

        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?”.

      • BB_C
        link
        fedilink
        arrow-up
        17
        ·
        2 days ago

        printf uses 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_start and va_end are?

        to get features that I normally achieve through regular code in other languages.

        Derives expand to “regular code”. You can run cargo expand to 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 named Debug which happens to generate “regular code” that implements the Debug trait. The proc macro named Debug and implemented trait Debug don’t point to the same thing, and don’t have to match name-wise.

          • hosaka
            link
            fedilink
            arrow-up
            4
            ·
            1 day ago

            For functions that want to accept variadic arguments in C/Cpp

            • RavuAlHemio@lemmy.world
              link
              fedilink
              arrow-up
              5
              ·
              1 day ago

              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.”

              • hosaka
                link
                fedilink
                arrow-up
                1
                ·
                5 hours ago

                Yeah, didn’t catch your sarcasm there :D

              • BB_C
                link
                fedilink
                arrow-up
                1
                ·
                1 day ago

                I was just referring to the fact that they are macros.

      • Marc@mamot.fr
        link
        fedilink
        arrow-up
        11
        ·
        2 days ago

        @cx40 @kornel using a macro ensure the format string and the values printed are typed correctly at compile time. It also ensure that most of the parsing is done at compile time and just the bare minimum is done at runtime. It’s a safe and fast way to solve this issue.

      • jutty@blendit.bsd.cafe
        link
        fedilink
        arrow-up
        6
        ·
        2 days ago

        Most high-level languages do a lot of things implicitly, like casting types, cloning values, deciding what the default is. Rust tends to avoid that, which though less convenient makes behavior more predictable, reducing footguns and surprises.

      • tatterdemalion
        link
        fedilink
        arrow-up
        2
        ·
        2 days ago

        Regarding the derive macros, there are a few reasons these are required.

        1. 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.
        2. 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.
        3. The derive macros are just a convenience. You are free to use “regular code” to implement those traits instead.