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.
  • @BB_C@programming.dev
    link
    fedilink
    242 days ago

    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?

  • @kornel@programming.dev
    link
    fedilink
    English
    18
    edit-2
    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.

    • @cx40@programming.devOP
      link
      fedilink
      English
      62 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.

      • 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@programming.dev
        link
        fedilink
        162 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.

      • Marc
        link
        fedilink
        112 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
        link
        62 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@programming.dev
        link
        fedilink
        2
        edit-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.
  • Sosthène Guédon
    link
    fedilink
    92 days ago

    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.

  • @BartyDeCanter@lemmy.sdf.org
    link
    fedilink
    102 days ago

    I think that really depends on your perspective. I’ve spent most of my career in C or a minimal subset of C++ for embedded systems. For me, even no_std rust has a rather rich set of features.

    • @cx40@programming.devOP
      link
      fedilink
      English
      32 days ago

      I can see that. I’m coming in from the other extreme that is Python, where even the meta-programming is done in plain Python.

      • I also come from python, actual meta-programming concepts are usually implemented via meta-classes, which I would describe as more complex than rust macros, in the “it takes longer to fully understand” sense.

        You could also generate python code and execute it if you don’t mind the obvious security implications, but that’s only a possibility thanks to it being an interpreted language, while rust macros can provide validations and readable error messages out of the box.

        To your post’s point in general:

        • Python uses exceptions, rust uses errors as values, making rust more predictable.
        • Python uses Nullables (None | T) which require hand-written handling, instead of rust’s Option<T> which has convenience methods and syntax support (... = maybe?).
        • Python has GC by default, and making a function fast requires delegating it to another programming language, meaning you usually would write python for software that needs not be performant.

        I have more points if you’d like to understand my position, and am willing to explain if you need.

      • TehPers
        link
        fedilink
        English
        42 days ago

        Part of why Python can do this is that it runs completely differently from Rust. Python is interpreted and can run completely arbitrary code at runtime. It’s possible to exec arbitrary Python.

        Rust is compiled ahead of time. Once compiled, aside from inspecting how the output looks and what symbol names it uses, there’s nothing that ties the output to Rust. At runtime, there is nothing to compile new arbitrary code, and compiling at runtime would be slow anyway. There is no interpreter built into the application or required to run it either.

        This is also why C, C++, and many other compiled languages can’t execute new arbitrary code at runtime.

      • @6nk06@sh.itjust.works
        link
        fedilink
        3
        edit-2
        2 days ago

        even the meta-programming is done in plain Python

        @decorator comes to mind, and only the keyword is part of the language.

        Common Lisp has meta programming built-in but no one uses Common Lisp for a good reason.

        • @solrize@lemmy.ml
          link
          fedilink
          2
          edit-2
          2 days ago

          Common Lisp has meta programming built-in but no one uses Common Lisp for a good reason.

          I use Common Lisp, but maybe you’re right and my reason for using it is bad.

  • @onlinepersona@programming.dev
    link
    fedilink
    English
    7
    edit-2
    2 days ago

    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.

    • @cx40@programming.devOP
      link
      fedilink
      English
      1
      edit-2
      2 days ago

      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! and format! 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.

      • @onlinepersona@programming.dev
        link
        fedilink
        156 minutes ago

        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.

      • @6nk06@sh.itjust.works
        link
        fedilink
        4
        edit-2
        2 days ago

        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?

        • @cx40@programming.devOP
          link
          fedilink
          English
          1
          edit-2
          2 days ago

          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.

      • TehPers
        link
        fedilink
        English
        32 days ago

        Through macros? The term “meta-programming” had me lost since I’m only familiar with that in reference to C++ templates (and Rust’s generics are more like templates).

        println! and format! are macros because they use custom syntaxes and can reference local variables in a string literal provided to the macro:

        let a = 2;
        println!("{a:?} {b}", b=a);
        

        I don’t know how the derive macros would be function calls. They generate whole impls.

        Macros generate new code. This is the same idea as C macros (except Rust macros generate syntax trees, not tokens, but that’s a minor difference).

        So to answer your question as to why there are macros, it’s because you need to generate code based on the input. A function call can’t do that.

        • @cx40@programming.devOP
          link
          fedilink
          English
          12 days ago

          The term “meta-programming” had me lost since I’m only familiar with that in reference to C++ templates (and Rust’s generics are more like templates).

          Yes, like C++ template and macros. The kind of code that generates new code before being run.

          So to answer your question as to why there are macros, it’s because you need to generate code based on the input. A function call can’t do that.

          You can design a language where you don’t need to generate code to accomplish this. My question isn’t why this is necessary in Rust. My question is why Rust was designed such that this was necessary.

          Someone mentioned elsewhere that this allows for compile-time type safety. I’m still trying to wrap my head around how that works.

          • @anton@lemmy.blahaj.zone
            link
            fedilink
            3
            edit-2
            24 hours ago

            You can design a language where you don’t need to generate code to accomplish this.

            Other people have python scripts generate C, so having on in the same codebase and language is certainly an improvement.

            My question isn’t why this is necessary in Rust. My question is why Rust was designed such that this was necessary.

            Because otherwise the compiler team either also needs to maintain a huge amount libraries or cut corners and move things to runtime that really should happen at compile time.

            Someone mentioned elsewhere that this allows for compile-time type safety. I’m still trying to wrap my head around how that works.

            printf is a great example. According to the C type system, it takes a string and a variable amount of untyped arguments dependent on the content of the string, but that doesn’t actually describe the allowed arguments.
            Misusing printf like this printf("%s", 42); will get you a warning, but only because there is a special case for printf in the compiler. If you have your own function that does the same as printf, and you misuse the same way, you will find out by dissecting the core dump.

            In rust the format string gets parsed at compile time by a macro, which decides the type of each arguments that can than be checked by the compiler. Imagine printf(“%s %d”,…) created a function with the signature specialized_printf(char* agr0, int arg1), it would be impossible to pass the wrong types of arguments.

            Now that these tools exist people have gone further and made a library that checks SQL queries against the shema of a running database and causes a compile error if it doesn’t fit.

          • TehPers
            link
            fedilink
            English
            3
            edit-2
            2 days ago

            You can design a language where you don’t need to generate code to accomplish this.

            Depending on what you mean by “generate code”, the only language at the level of C or C++ that I can think of that does this is Zig. Zig is weird though because you’re still doing what is functionally compile-time reflection, so in a way you’re still generating code, just in a different way.

            If you’re comparing to Python, JS, or even C#, those all come with runtimes that can compile/interpret new code at runtime. None of those languages are comparable here. Rust, C, C++, Zig, etc compile into assembly, and type information, impl information, etc are all lost after compilation (ignoring symbol names or anything tracked as debug info).

            If you’re specifically referring to Debug, Display, PartialEq, etc then the compiler doesn’t do that for you because Rust doesn’t assume that those traits are valid for everything.

            Unlike Java where new Integer(1) != new Integer(1) or JS where "" == 0, Rust requires you to specify when equality comparisons can be made, and requires you to write out the implementation (or use the derive for a simple, common implementation).

            Unlike C# where record class Secret(String Value); will print out the secret into your logs when it inevitably gets logged, Rust requires you to specify when a type can be formatted into a string, and how it should be formatted.

            Just because a language does things one way doesn’t mean every language ever should do things that same way. If you want it to work like another language you like to use, use the language you like to use instead. Rust language designers made explicit decisions to not be the same as other languages because they wanted to solve problems they had with those languages. Those other languages are still usable though, and many solved the same problems in other ways (C#'s nullable reference types, Python’s type hints, TypeScript, C++'s concepts, etc).

  • @chaos@beehaw.org
    link
    fedilink
    42 days ago

    A minimal but powerful language can feel like magic. Like, literally. The whole appeal of magic in stories is that you can step out of the normal rules and do something that defies belief, and who hasn’t fantasized about that in real life?

    If the language you’re using has a lot of magic built into it, things that the language can do but you can’t do, you feel mundane, like the language is letting you look at the cool things it can do, but doesn’t let you do them yourself. A more minimal language, where the important things are in the library, means that the language designers haven’t kept that stuff to themselves. They built the language such that that power is available to everyone. If the language gives its standard library authors the power to do things beautifully and elegantly without special treatment, then your library is getting those benefits too.

    It’s also a sign of good design, because just saying “well, this thing magically works differently” tends to be a shortcut, a hack, an indication that something isn’t right and couldn’t be fixed nicely.

    • TehPers
      link
      fedilink
      English
      22 days ago

      While I agree with your post, I do want to call out that Rust’s standard library does use a lot of unstable features and calls compiler intrinsics. Anyone can use the unstable features I believe with just #![feature(...)], but not the intrinsics (not that there’s much reason to call the intrinsics directly anyway).

  • Matt
    link
    fedilink
    22 days ago

    Like what? If you need something that the language is missing, then just open crates.io (or lib.rs, which is subjectively better frontend for browsing crates).