Why not just add classes?

Worth note along the way that while macros are way better in Rust than in C or C++, they're still a bit of a mess. See some of the discussion in this thread, where I got schooled on a whole bunch of those limitations, for more. See also Nick Cameron's blog posts about his hopes for substantially improving the macro system over the year ahead.

On the OP: Rust isn't just a "better C"; it's at least equally what Sean Griffin has called "a more practical Haskell." The syntactical similarity to C-descended languages (and current marketing, which I fully appreciate) notwithstanding, Rust borrows a lot of ideas from Haskell and ML-family languages in its approach to code reuse, and to types in general. So it's a real shift paradigm-wise from the C++, Java, or C# styles of inheritance (or even Python's), but one that, once you get used to it, makes the old version feel like a straitjacket when you go back.

7 Likes

It looks like it might all be there but breaking it up seems to take something simple class myclass{...} and make it really complicated (structs, traits, impls and self).
It's clear that this was a design choice and not a committee-based,
ball of mud happenstance (I'm looking at you C++) but I don't understand
the rationale behind this design choice. What does not having
"traditional" classes buy Rust?

I primarily come from a C++ background and from a purely syntax point of I actually find the struct/impl/self approach superior to a single class layout in many ways.

For one, separation of functions and data. In C++ as classes grow, unless you have a strict coding standard they often become a mess of members and functions in no particular order. Separating them helps with readability and consistency. Separating data from functions helps focus on the data, which is important from a performance perspective. You also need to be aware of member alignment in C++, so having data grouped together, separate from methods again makes it easier to read and arrange your members.

I'm also a fan of self, maybe because I'm used to it from Python. it makes a clear distinction between struct members and locals. In C++ again this usually is addressed by enforcing a naming scheme for members, commonly m_ prefix or _ suffix. Also the lack of self replacing the need for static methods feels more natural to me.

Hypothetically it would be possible for C++ to adopt impl and self instead of defining methods in classes without changing C++'s functionality and I think it would be an improvement.

3 Likes

Since we seem to be going there anyway...To be honest with you macros are always a mess and nothing will ever fix that. I'll take your word that Rust macros are better than C/C++ but that is setting the bar so incredibly low to really be a distinction that isn't worth much of anything. It's why I like my neutron bomb analogy. The world came together, looked at that weapon and decided that while, yes it would be very useful and yes it would be efficient it's destructive power is just too seductive. Granted it helped that everyone was convinced that building one wasn't possible but still. Macros are the same thing to me and I wish the software engineering world would do the same thing. Whatever marginal good macros might do is far out weighted by the destruction they inevitably cause.

I understand the rationale between Rust's removal of classes, but I still occasionally think that having a way to say "alright, I want a type barthat behaves like a foo except for this specific method", without having to manually implement the traits/methods, would be pretty nice. (Actually, I think it's quite possible to have something like that using Derefbut I remember reading that it was wrong to use it that way, though I'm not sure why?)

Now, there are some upcoming features (such as specialized impls) that might address (at least part of) this without using classes, but I still think that's something that's missing in current Rust.

1 Like

@elizabeth_henry Personally, that isn't an issue I've found when thinking about problems from a compositional instead of an inheritance-based mindset.

@Pointer2Void As for macros, Rust is the first language I've used that has them, and have found them great for writing simple DSLs. Are you taking offense to them from a conceptual or implementation point of view? Is it macros you hate, or the way they're implemented?

1 Like

I'm not quite sure what you mean by "...way they're implemented" but I willing to have my answer be: Both.

Like I said earlier, it may just be my C++ experiences (and those of my friends) and maybe Rust macros really are different. That said, C++ macros SUUUCK. I have wasted so much of my life stuck in macro hell because somebody decided they wanted to be lazy (I'll take Go levels of verbosity over "clever" macros all day, everyday, twice on Sundays) or "efficient" (not to be confused with actual efficiency) or "they had to" or because they take it as a point of pride writing dense, impenetrable, unmaintainable, untestable, unreadable code (I worked with one idiot who seriously called it "job security"--I work at a large software firm). And I just find it really hard to believe that if Rust gains widespread adoption that Rust macros won't be abused in the same way that C++ macros are. In fact, it took me all of 5 minutes to find a D supporter talking (bragging?) about it in this very forum. Here's the money quote:

I don't do systems programming (i.e. embedded systems, OSs or drivers). I do servers and applications but I agree with Herb Sutter's remark that the rise of cloud apps (and collapse of Moore's Law) puts a premium on smaller footprint dev technologies that have good concurrency support (like Rust seems to). But reading quotes like above and all I think is "the JVM or CLR aren't that big" :laughing:

1 Like

I personally think that Rust macros just can't be abused like C/C++ macros. They have a lot of restrictions on what you can pass as their arguments and on what they can produce, and they also do not perform textual substitution, they work with ASTs/token trees and are hygienic, which alone severely limits the amount of craziness you can do with them. The only abuse I can think of which is possible with Rust macros is implicit altering of control flow (like in try!), but I doubt that it would be a widespread practice because Rust is much more expressive than C in terms of local control flow (if let, match, soon-to-be-added ?/catch, etc.), macros are just unnecessary in most cases.

Of course, you can do crazy things with Rust macros if you really want to. There is even a book about how to write complex macros. However, this is highly non-trivial work, and anyway most of time all effects of a macro will be localized to its invocation point where it is clearly visible that it is a macro, especially with syntax highlighting.

Consider, for example, this library. It provides a single macro which allows to eliminate a lot of boilerplate. It is very complex internally, I doubt I would be able to write something like that in a reasonable amount of time. However, I would strongly disagree if you call it an "abuse" of macro system. After all, the effects of this macro are localized to its invocation point, and it does its work perfectly. And this is how most of macros in Rust work, or, at least, are intended to work.

3 Likes

I assume you mean the preprocessor by "C++ macros". Personally I think template metaprogramming has the same potential for causing an unpenetrable mess compared to classic preprocessor macros. Note that I said potential, of course many template (and macro) facilities are used as they should: to reduce programmer load and solve don't-repeat-yourself problems. As @netvi shows in one of many examples.

Every language powerful enough to do that has metaprogramming facilities, and you'd have to damn every one of them because of the potential for abuse. And you can write "job security" code in any language.

4 Likes

My problem with macros can basically be boiled down to this: I consider macros some kind of co-language. They allow a restricted form of code generation/manipulation. If the facilities the macros provide are necessary to write expressive code, why are they not part of the "main" language? Macros always directly point to deficiencies in the core language.

5 Likes

If the facilities the macros provide are necessary to write expressive code, why are they not part of the "main" language?

Maybe because it is impossible to put everything necessary to write expressive code into the core language? Regardless of how expressive your language is, some things would still be impossible to express in it. Even Haskell has Template Haskell extension, which is essentially a macro system. When a language has macros, it allows its users to extend the language in the ways which would likely be too difficult for the language to support (and not only in terms of manpower of language developers, but also because lots of features means more complex language which is harder to learn), and I personally think it is a good thing, especially when this extensibility has well-though boundaries, as in Rust case.

2 Likes

There's quite a couple of very successful languages that do very fine without macros, for example Ruby, Python, Java, PHP, JavaScript and C#. None of them are known for lacking expressiveness. Especially modern Java lost a lot of the "verbose and boilerplaty" air around the language. It is definitely possible to write richer core languages that allow for all these things without macros. And not all of them are built by huge teams.

2 Likes

Sorry, but adding languages as dynamic as Python to this list makes absolutely no sense. As an example, Python's metaprogramming facilities are numerous (generating and executing code at runtime being among them), there's absolutely no need for macros there. So please don't compare apples and oranges.

4 Likes

I disagree. Languages provide access to the underlying runtime features of a language. Python provides convenient access to all runtime features of Python, one of them happening to be runtime code compilation and extreme late binding.

Obviously, there is no feature parity on the runtime level here, but the Rust language and Haskell languages do the same: they provide access to their underlying runtime features. The fact that a macro language is involved at some point (IMHO) points to the fact that they lack the convenient part in the core language.

1 Like

Rust and Haskell have a type-system. Every function must have a type and is restricted by that type. Some kinds of things (derive, format!, variadics - e.g. vec!) do not fit that system well. Therefore you need to use macros to metaprogram over them.

Of course, some macros are because we can't be generic over some things - integers, mutabilites. But that does not mean they all are.

Python allows you to do all compile-time things at run-time. so you don't need a macro system. Even C#/Java are more dynamically typed than Rust (they have Object and reflection).

2 Likes

Moderator note: A gentle reminder to all: the Rust forums aren't the place to do language bashing. Constructive comparisons are of course a-okay!

4 Likes

But that still points to a hole. I could still implement things like format! by hand. (tediously, though a chained API)

(Python and Ruby have "a type-system" too, by the way. They just bind very late and don't enforce at compile-time. I would even argue Ruby has as many implicit conversions as Rust (almost none)! For a lot of thinking around this, I can recommend the following text: http://www.ics.uci.edu/~lopes/teaching/inf212W12/readings/rdl04meijer.pdf)

But many are. Look at all the parser combinators like nom. They are pretty much huge code generators avoiding a lot of busywork. Serde has the same: it employs macros to avoid defining and hand-crafting the components needed for proper deserialisation to make the easy case easily accessible and hide the complexity.

I disagree on the point that you don't need a macro system because of the dynamic nature of the runtime system. I'm pretty much aware on where C# and Java fall on that scale, but for example the dynamic nature of Java is mostly reached through late binding together with reflection, an approach that is very much possible with Rust through dyld and looking at symbols of a loaded library.

1 Like

I was using "type-system" in the compile-time sense - each Rust expression must have a type that the compiler infers. This means that format must have a type. But format can't have a simple type (because it is variadic etc.), so it has to be a macro (or you can do the "array-of-Object" translation, but that loses you things).

From what it looks like, nom mainly uses macros to allow for top-level type inference - that's certainly another annoyance I forgot about. Many parser generators in other languages use reflection, which I'm not sure is better than syntax extensions - at least with syntax extensions you have the type-checked post-expansion code.

dyld can't interact with types.

Well, I got this problem playing with the hoedown bindings: it's pretty easy to use the Html renderer, but let's say you're globally happy with it but just want to change the way it handles, say, paragraphs. You have to custom struct (ok, seems unavoidable):

struct MyRenderer {
    html: Html
}

then implement your modified version for the paragraph function of the hoedown's Render trait:

impl Render for MyRenderer {
    fn paragraph(&mut self, ob: &mut Buffer, content: &Buffer) {
        //stuff
    }
}

Ideally, that would be all. But in the current state, you also have to manually delegate each of the other methods of the trait to the html field of the custom struct, which isn't that hard:

fn emphasis(&mut self, ob: &mut Buffer, content: &Buffer) -> bool {
    self.html.emphasis(ob, content)
}

Except you have to do it for something like 30 methods. Which quickly becomes a bit tedious and makes me wish there was some option like this RFC for automatically delegating trait implementation.

2 Likes

You're right there, following the trail of the RFC you linked looks like it's in progress though. I'll be very happy once that's in.

2 Likes

Indeed. And how the language designers implement metaprogramming facilities is everything. By all accounts Rust has very good PL people deciding what's in the language, which does make me optimistic about the future of Rust. Though C++ acts as a warning as to what happens if you get metaprogramming wrong. They did a terrible job of it and of course it didn't affect the adoption of modern C++ at all everybody just licked their chops and said "Yea! More ways to right byzantine code! And we get page after page of worthless stacktrace when something goes wrong!?! Sweet!". I guess at core the thing that is drawing me to Rust is the potential to get C++ level performance and flexibility without the "design by committee", ball of mud C++ as become.

In my opinion macros are unnecessary. I know that ship as sailed with Rust but I hope that whomever decides (the fewer people the better) what's included in Rust appreciates that just because something is "useful" does not necessarily mean it needs to be in the language (Ken Thompson is absolutely right about Stroustrup--he doesn't know how to say "no"). Trying to please everyone all the time is just a recipe for disaster.

2 Likes