Why not just add classes?

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

Re: JS - There's an AltJS language called Sweet.js that is a superset of ES5 with the addition of macros. The macro system is similar to the one used in Rust IIRC.

I find it useful for getting around pain points in the language. Especially writing test code that doesn't have a lot of function () { boilerplate everywhere.

Subclassing (i.e. virtual methods) is an anti-pattern that is (and often a highly destructive) premature binding.

  1. We can't express relationships due to invariants of the Liskov Substition Principle, e.g. the Square and Rectangle relationship.

  2. Expression Problem as framed by Wadler. We can't implement a new method on an existing type without adding the method to the preexisting type. Thus for example, if some library returns an array of Widgets and we want an array of FastDisplayables, and we can't edit the source code for that library and recompile, then we have to create a wrapper class and rebuild the entire array. Whereas, by separating implementation of interface from data, we can reuse the data and create a new interface to "view" that data with. I claim Rust has ad hoc polymorphism similar to Haskell's typeclasses, which is one the main features drawing me to investigate Rust.

2 Likes

I like the gist of macros in Rust, but I would like them to become more like macros in Lisp.
That would be really great :slight_smile:

The reason why macros are great - and needed - is that we can use it to extend the language (add new syntax) - and as such, I don't view macros as a kind of hack.
To the contrary: it is one of the niftiest features of Lisp.
In Lisp, you use macros to write a (new) language specifically tailored to the job at hand.

1 Like

You are right. Rust's traits are absolutely an implementation of type classes, though there are some differences from Haskell's implementation.

1 Like

Thanks for the confirmation. I was hoping to get some confirmation. I am surprised afaics the ad hoc polymorphism is not mentioned any where in the documentation. And the Expression Problem nor the Wikipedia entry on Composition over Inheritance, are also ostensibly not mentioned in the documentation. For me, if considering Rust as potentially a better "high-level" language, the ad hoc polymorphism in a language which does not have Haskell's coinductive type system, seems to be unavailable in any other C/C++ derivative (potentially mainstream) language?

I'm suggesting the documentation maybe could be improved to proactively explain to incoming OOP (aka subclassing) converts, to make an argument for why they typically don't want to be using the anti-pattern of OOP virtual methods, and instead using late binding dispatch at the call site, instead of at the declaration site. In other words, ad hoc polymorphism un-conflates (makes) interface from (orthogonal to) data type, and the binding of the interface to a data type occurs at the function call site, not at the data type, interface, nor function declaration sites. Of course there are some tradeoffs, but the inflexibility of premature binding is removed.

For a mainstream high-level language, I am starting to contemplate if I am wishing Rust's ad hoc polymorphism was available in a strongly type language that had no verbosity GC and didn't basically force on us by default the noisy and complex type system of modeling lifetimes and memory (which apparently even infects generics with the 'a syntax ... I haven't learned that yet though). The lifetimes and memory allocation feels too heavy (a PITA) for a language that most programmers would want to use most of the time. Sometimes you want that control, but always by default? And a mainstream language without first-class (i.e. not a library) async/await is becoming an anathema.

In general, we try not to compare Rust to other things, only explain it on its own merits. I think some other supplimentary resource that does this would be interesting to read, though.

4 Likes

Let us not forget this section on Rust Learning where Rust gets compared to a lot of languages: GitHub - ctjhoa/rust-learning: A bunch of links to blog posts, articles, videos, etc for learning Rust :slight_smile:

I was about to start another discussion about this, but from a different angle;

I'd start with the pragmatic observation: there is a lot of existing working software that can take internal-vtable based plugins,

.. and ask if there's a way that the trait-viable mechanism could be generalised to the level that the vtables could be anywhere ((i) fat pointer, (ii) embedded as the first item of a struct, (iii) or calculated by some generic means from the address and other information in the struct - imagine for example being able to retroactively get from an enum tag to a vtable. apparently some java implementations don't actually store a vtable in the object like C++ but rather store them at the start of contiguous arrays of homogeneous objects.. thats actually quite interesting. maybe a way to leverage the MMU, mapping ranges such that calculating the vtable ptr is very easy.

Let me refresh my memory, I think there was some highly unsafe hack that almost allowed internal vtables... I remember some messing around with cast::transmute and the deref traits , which got to one level; the question would be 'could extra language support allow this to be done in a less unsafe way' - exposing a way of binding a vtable with the underlying data, or querying the vtable of a trait on it's own.

(I can't remember off hand if rust even has plain function pointers these days, i realise you could roll them with that)

r.e. "classes", I haven't tried but i'd imagine you can make macros that declare things more concisely.

tangent;-

whilst rust macros are really useful, macros generally spike a negative reaction in my head on 2 fronts:-

  • it might be C prejudice, but the idea that they are a 'hack'
  • further justification: language features can be reasoned about at compile time and get much better error messages; they can be written using the rest of the language's syntax, as such I think 'a more powerful language' is a more efficient use of the whole world's resources. when using macros, it's usually for something another language can do better with a 'more solid' feature.

-plain syntax issues which might be fixeable through other requests:
the extra nesting level and disruption of existing declaration pattern really bug me (i wish it didn't). It's because Rusts' "pattern" of declarations is

defining_keyword name {
    content 
}

you can't replicate this with a macro; as such macro-base declarations stick out from the rest of the language like a sore thumb.

( I know lisps make great use of macros but there everything looks the same..)

if you could write

class!  MyCppStyleObject {
    ...
}

that might help a lot perceptually (a preceding ident in macro-rules?).. but I've no idea how feasible it would be r.e. parsing though (maybe it would be asking too much to be able to allow anything between the ident and the 'main body', as we'd need if we wanted to write class! Foo : Bar { ..}

back to the topic of the thread,
r.e. the mention above that these vtables are an 'anti-pattern' (premature binding?) I think it's a case of "not throwing out the baby with the bathwater" .. the fact is they can still be useful.. there's a sliding scale of runtime efficiency, syntactic convenience, versatility . I would personally like to see a sliding scale of versatility where you don't have to bake any one path early on into the syntax. 'openly derivable classes' can still be used for code that queries the type (e.g. if (auto *p=dynamic_cast(x)){ .. } etc etc) . you could imagine 'rolling an enum' being like 'declaring a base class and a fixed set of derived classes', and you could use it both ways (e.g. still derived new classes, still make vtable entires for the enum..). (imagine, coming at it the other way, if you could generalise computing the tag of an enum.. such that the tag could just be an internal vtable pointer .. an enum could even implement itself as a class by doing a whole-program 'gather' of all the useages?)
Such blurring of the options might have utility in refactoring and in experimentation, e.g. a sourcebase might go through various options until it discovers the right layout

1 Like

Classes irk me, and I come from C++. At the same time though, when I want to "inherit" fields, it does feel odd to do something like this:

struct Student
{
    base: Person,
}

What would be nifty though is syntax like the following, where I don't have to specify student.base.name but rather student.name:

struct Student
{
    self: Person,
}

Maybe multiple base structs could work? That is, provided there's no field name clashing, but I see this as an extremely rare occurence:

struct Pegasus
{
    self: (Bird, Horse),
}
3 Likes

I agree with this (although I'm also open to the suggestion that inheritance can be improved on, the hierarchical idea is limited),

I've always seen single inheritance as a low-level feature - something that is possible in ASM because you know the layouts can just overlap; so you have a neat, efficient shortcut for a tree of variations with common 'earlier elements' ..

one suggestion I had was to use 'Tuple Structs' to do some of the job of field inheritance.

a Tuple Struct has no named fields; but it does have an order.
imagine then if any attempted named-field access to a tuple-struct actually searched it's components, prioritised according to the order in which the types occur.

but ultimately it would be better IMO to go with principle of least surprise and allow the straightforward struct Foo : Bar {..} syntax, and maybe figure out something logical to do with struct Foo : Trait {..} (sugar for impl Trait for Foo inplace, the case of a struct having at least one trait... so make it easier to roll that). IMO struct Foo : Bar{} happily fits with the rest of the type syntax.. and it's extremely common in other languages; let x:Bar - "x is of type Bar".. struct Foo:Bar - "Foo is of type Bar"

Many people say C++ has mis-features, but IMO it's that just an omission of alternatives that mean it's 'core-features' must be stretched in awkward ways (no multiple-dispatch -> use verbose double-dispatch to fake it.. but that doesn't mean the inbuilt single-dispatch was a bad idea, in the case when you do know a certain set up front.)

4 Likes

I tried to use Rust today, and after Swift this language looks like C with security features. I guess someone must develop Rust++ and add OOP.

I would suggest giving the language a bit more time before emitting this judgement. And looking up traits.

9 Likes

Rust was designed with a "composition over inheritance" point of view, so it's similar to Go in that respect. It's most definitely more than just a "C with security features", Rust is more like a high level programming language which lets you use a lot of the powerful functional programming patterns you may use in Haskell, Python, or JavaScript, while also giving you the ability to do low level/high performance things like you would in C++.

In particular you may find this quote from the second post in this thread interesting:

7 Likes

HadrienG, Michael-F-Bryan, I got your point, but I just expected Rust to be more high level language than it is.
Syntax for OOP in Rust reminds me C-style implementations like GObject.
I am not saying that inheritance is necessary, or Java-style OOP is always right, but from Swift or Kotlin programmer OOP syntax looks archaic, and it's going to be hard for not C-people to transfer to Rust.

Swift:

class MessagePresenter {
	func show(message: String) {
		print(message)
	}
}
let messagePresenter = MessagePresenter()
messagePresenter.show(message: "Hello World!")

Rust:

struct MessagePresenter {
}
impl MessagePresenter {
    pub fn show(&self, message: String) {
        println!("{}", message);
    }
}
fn main() {
    let message_presenter = MessagePresenter{};
    message_presenter.show(String::from("Hello World!"));
}
1 Like

it does seem like layering traits can often handle the use cases of inheritance but in a more general manner (a trait with a default impl relying on another with a load of getters/setters isn't far of).. perhaps there's some tutorial somewhere that would spell it out.. 'here's something done with inheritance, here's the approach with traits').

i am sympathetic to both POVs on this.. with traits/generics/enums as the starting point you've wiped out some of the use cases of inheritance.. you can simplify some interfaces by switching on enums. you can make generic 'impls' that seem to achieve 'layered interfaces' in quite a pleasing way. 'here's the interface for a window.. here's the interface for a grid view.. here's a generic allowing anything satisfying grid view to satisfy window')

on the other hand inheritance could be seen just as a handy syntactic shortcut in the case of '1 struct, 1 interface' (which you could still expand upon), and 'an embedded vtable' is a means of having a polymorphic object of variable size , located by a single pointer. (enums are padded .. it might be interesting if rust eventually gains an ability to express an immutable-tag enum without the padding)

@demensdeum, I'm not sure exactly what you mean by "OOP" here. My interpretation of object-oriented programming is that you can have types which bundle some data and also allow you to call methods on that type which let you do operations on the bundled data. Whether you use the class keyword or struct is just a syntax detail.

The big thing with OOP is the ability to have polymorphism, i.e. being able to use one type in the place of another type transparently as long as they specify some sort of interface. Traits and inheritance allow you to do this, it's just implemented in different ways. Rust does things in a different way to C++ or Java (which is why it's called "Rust", not "C++++"), but I wouldn't necessarily say one way of doing things is better or another is archaic.

I think you'll find that differences like methods taking an explicit self parameter instead of the implicit this in Java or C++, or how you'll define the contents of a type separate from the behaviour (the separate impl MessagePresenter block) are only skin deep. You still often use similar patterns to "normal" OO languages, it's just that you'll use composition more and not have a deep type hierarchy.

I'd agree with @HadrienG. It always takes a bit of time to learn a new language and you often experience a bit of friction because you come in with a bunch of assumptions from your old language and find that those patterns aren't idiomatic in the new one. Before learning Rust I did a lot of work with Python and C++, and it took me a while to get used to how Rust does things.

My advice would be to go along with it for a bit longer, try reimplementing some of your old applications in Rust to get a hang of the language, and don't be afraid to ask questions like "is this idomatic?", and try that for a couple weeks before deciding it's not high level enough. For example, here's a library for generating LaTeX documents I made. You'd be hard pressed to find something more high-level in Python or Ruby.

10 Likes

"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"

Sure but that is a terrible argument against macros and preprocessing generally.

It's a use case issue. I just created composite objects in a script language without conditional logic (it uses includes.) Macros and external scripts and executables are assumed.

Let me take the rationale into the domain of linguistics and philosophy.

Data and functions (actions, changes, relations) have fundamental differences. That's why they exist in their forms.

Data is declarative as are macros which reflect static and declarative function types. Very Rust consistent.

We may ask what inheritance reduces to but here's a thing...

Rust's Traits are a close analog to Chomskian language Features.

In this paradigm all language is universal but ordering is flexible as is semantic composition of parts of speech.

In language production concepts get a base grammatical structure and features (tags, traits, clauses) get added along the way to utterance. Where appropriate.

I therefore psuedo-conclude that Rust conforms to existing cognitive science.

In doing so I infer that objects and oop generally are indeed equivalent in use of composition.

Sadly, Traits appear to be potentially verbose (like interfaces) and wrapping an external object labour intensive.

I guess that is why it's called work. Coming soon... Satire!

1 Like