Why not just add classes?

So Java and C++ are the languages I use professionally (mostly Java these days) and I came across Rust, looked interested, I'm a language nerd so I did a bit of reading and I hit this forum post. I found Ygg01's answer interesting because it seems like a long way to go to not just add the keyword class to the language.

So was the point to have Rust be a better C and so if you want OOP look elsewhere or is Rust creating another way to do OOP and I just missed it? 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?

6 Likes

@nikomatsakis wrote a series of articles about classes (albeit from the perspective of type variants) in Rust. They're a worthwhile read.

http://smallcultfollowing.com/babysteps/blog/2015/05/05/where-rusts-enum-shines/

6 Likes

Why add classes? If it's just to avoid writing the fields separately to the methods, then one can easily get that syntax with a macro (although just getting used to the plain Rust syntax is probably better long term). If it's to gain the ability to get polymorphism and shared behaviours via inheritance... well, that's somewhat missing the forest for the trees: the actual problem to solve is polymorphism, and inheritance is just one tool for that, there are others.

Rust's approach of separating data (struct & enum) from behaviour (impl) and using trait to group those behaviours for polymorphism can feel like a paradigm shift from the conventional inheritance-heavy approaches of Java/C++, and hence requires some getting used to. The mindset required for this (and one that is in fact often espoused as good design even in Java and especially C++) is often referred to as "composition over inheritance". (Also, don't miss enums in Rust, which really nicely solve closed polymorphism, i.e. situations where an inheritance hierarchy is fixed and won't be extended.)

Lastly, note that having explicit self arguments on methods is somewhat orthogonal to classes vs. no classes, e.g. Python has traditional classes and yet the receiver/self/this of a method has to be explicitly listed. That said, in Rust, it is actually quite useful to have explicit self because that argument can be passed in multiple different ways, e.g. self vs. &self vs. &mut self. (C++ does something somewhat similar, by having the const and mutable specifiers that can be attached to methods.)

34 Likes

First of all: Rust used to have classes. They were removed because other concepts were seen as more fitting. That happens to be the case for many aspects of the language: Rust also had a GC.

The issue from my perspective is: if you add classes, Rust would have classes and traits. It wouldn't solve much. And separation of implementation and data has some really neat advantages: for example, passing data over FFI boundaries becomes far more natural, in both ways. As long as you know the data layout, you can use this data in Rust like any other. (Just implement traits on top of it)

I do a lot of FFI work and am surprised how easy it is to wrap foreign code into a Rusty interface.

It's a little bit more fragmented, but in a good way for my taste.

Finally, the OO-nerds perspective: there are about a hundred opinions and implementations of what proper OO is (from CLOS over Java/C++ to Ruby and Smalltalk message passing), Rust adding number 101 isn't much of a problem.

17 Likes

I first heard about that in McConnell's "Code Complete". When I was a professional cell biologist I remember thinking, "So basically the argument is that some engineers design brittle structures that turn into mud. Pssf, hire better people." Now that I'm a professional software engineer I have a great deal of sympathy for the notion of protecting developers from themselves. Pressure from sales, from management, from customers is more than sufficient to make good engineers do really terrible things. Which is what made the following quote interesting:

Maybe it's just memories of C++ but the amount of damage caused by macro abuse make a brittle inheritance structure seem trivial in comparison (and let's keep it a hundred, Rust devs will abuse the hell out of macros and make all kinds of terrible, nightmare-ish, impossible to maintain, abominations with Rust macros). I know Rust macros are different but from what I've read so far not different enough IMO. Macros should be banned for the same reason neutron bombs were banned. The temptation to use them is too great (see previous comment about management, sales and customers) and the damage greater. This should probably be a separate thread, sorry. I have a lot of "charlie in the trees" issues (as well as a few rational issues) with macros.

I'll resist the urge to post the XKCD link. :wink:

2 Likes

Thanks :). I really like the point and the direct addressing of the problem. Also, banning of neutron bombs!

:+1: because it makes it easier to resist the urge to tell you how much I dislike it :D.

3 Likes

Nice read. Is something coming out of those unsized enums/thin traits ideas?

Nice read. Is something coming out of those unsized enums/thin traits ideas?

I believe that specialization was seen as a prerequisite, because the major proposals rely on it. So we had to nail that down first.

1 Like

Perhaps this is getting a little off-track, but I want to defend macros here. Rust's macro system is similar to (and based on!) that of Scheme and Racket. They have an even more extreme case: macro invocations and function calls look identical to each other, partly because so much of the language is macros that it would be distracting to mark each macro invocation. And yet those languages work quite well! And the users of Rust's macro system have done good things with it so far.

And I think having a macro system is great for cases like this: you can write a class! macro and then see whether it makes your life better. There'd be some danger that it'd make it easier to forget, say, that it's possible to impl a primitive or a type that you've used from somewhere, but the danger of forgetting is always present, and I think it's worth it for the opportunity to directly experiment with this language design question.

4 Likes

I didn't realize Rust allowed their macros to reach that deep. That's good information. Thank you.

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