Hacker News new | ask | show | jobs
by SMFloris 1881 days ago
Recently experimented a bit with Rust and I found the reverse to be true. You cannot compose types in Rust. You compose behaviors not types. Very important distinction as I found out the hard way.

Take the following example I have found on the net: https://play.rust-lang.org/?version=stable&mode=debug&editio...

In that example, both the bicycle and the car have the property `speed`. Imagine you have multiple types now that need to have the `speed` property. You would need to copy-paste the same code for each new type in order for you to be type safe.

Apparently it is called *Monomorphization*: https://cglab.ca/~abeinges/blah/rust-reuse-and-recycle/#mono...

From the article:

* But if I want a single queue to be able to handle different tasks, then it's not clear how that could be done with monomorphization alone. That's why it's called "mono"morphization. It's all about taking abstract implementations and creating instances that do one thing. *

Which was exactly what I was experimenting with: A single queue worker that can handle different cases. Honestly, it made Rust almost not worth it for me. Sadly, I was too deep to turn back so I wound up doing the whole thing in Rust. I have tons of copy-paste code. It is ugly and it is bothering me.

7 comments

You actually can have a single queue that handles the different cases. You want dynamic dispatch for that, the syntax looks like this (quick addition to the playground sample): https://play.rust-lang.org/?version=stable&mode=debug&editio...
... which does put both cars and bicycles in the same queue, but doesn't eliminate the copy-paste for each new type completely; 'car' and 'bicycle' still wind up with separate 'get_speed' impls, which are textually identical aside from the type names.
Sure, traits talk about functions, not properties/ strict fields, so you need to provide trivial getters if you want to abstract over properties.

True, but not that interesting? Sure we could have some Ruby :get_attrs magic or whatever.

Usually the way people solve this in Rust is with macros.
I've heard "monomorphisation" to refer to something a compiler does, but not something a programmer does. I think this is just "repetition"!

The need for something to solve the problems inheritance solves has been known in Rust for a long time. Mostly it's been motivated by the need to implement the HTML DOM, which is fundamentally an inheritance hierarchy, in Servo. There's a longstanding RFC about it:

https://github.com/rust-lang/rfcs/issues/349

Inheritance is one possible solution. There are others.

I do hope they adopt one solution or the other. Rust for me seems incomplete because of this.

Another issue I have with it is perfectly described in this article: https://theta.eu.org/2021/03/08/async-rust-2.html

For reference I have over a decade of JavaScript experience in industry and my async Rust rewrite of a large JS project was *more* concise then the heavily refactored and polished NodeJS version (a language I consider more concise then most). If you are having to copy and paste excessively in Rust that is an issue but it is not necessarily intrinsic to the language.

For what it's worth traits largely prevented copy and paste and where traits fail there are macros. The classic inheritance example you link to is a tiny percentage of my code and an orders of magnitude smaller time sink when compared to the code maintenance problems I faced in other languages.

Monomorphization is not a user action of copy pasting, it's something the compiler does with parametric code. If we're talking about Rust it means when you write:

    fn id<T>(t: T) { }

    fn main() {
       id(String::from("foo"));
       id(1_usize);
    }

The Rust compiler will generate a method of `id` that works for both String and usize, so there will be two copies of the function with a slight variation in your binary. That process is called monomorphization.

> Which was exactly what I was experimenting with: A single queue worker that can handle different cases. Honestly, it made Rust almost not worth it for me. Sadly, I was too deep to turn back so I wound up doing the whole thing in Rust. I have tons of copy-paste code. It is ugly and it is bothering me.

You can easily use generics, there is no reason to copy paste code like this

Would that copy-paste code ever amount to more than some getter methods, like the get_speed() example you provided? If the speed field had some special behaviour, couldn't you wrap it in its own Speed type, with its own methods, and use that from the enclosing types?
I think the solution you've proposed is probably how you'd do it - but isn't inheritance more elegant in this case (i.e. using a language which supports inheritance if this is important to you)
No, inheritance is not an elegant way to mix in shared data and behavior. It can seem like it is in a simple case where there is only one set of data and behavior you want to mix in, but it scales very poorly. Nothing is fundamentally a single kind of thing, and multiple inheritance is a mess. It is more elegant to mix the behavior in through delegation, because it scales to however many things you want to mix in. Carrying on the synthetic example in this thread, you can mix in Speed and Pedals into Bicycle but Speed and Cylinders into Car.
Maybe not on every situation, but I do agree that inheritance is more elegant in a lot of situations, in the sense that it gets some behaviour "out of the way".
Sorry, I'm not getting it -- could you please explain further?

> You cannot compose types in Rust. You compose behaviors not types.

But I thought in OOP behaviour is type? (Or, IOW, type includes behaviour.) To me, judging only from this, it feels like Rust isn't quite OO... Is it perhaps just not quite finished yet?

Another aspect: That whole "Rust has something called 'monomorphization', which leads to lots of copy-pasting" reinforces that impression. Is this the same problem that C++ tries to overcome by the "select which inherited implementation to use" operator, and other languages by allowing inly single inheritance?

Beyond some small changes I would make to use new-types all over the place, I personally like to rely on Deref impls to mimic inheritance (although not everyone agrees this is a good idea to do too often): https://play.rust-lang.org/?version=stable&mode=debug&editio...

As you can see I also used a macro by example there to remove some of the duplicated code that you would otherwise have.

You can use a macro instead of copy and pasting.

You can also do this:

  struct Vehicle
  {
    speed: f32,
    type: VehicleType
  }

  enum VehicleType
  {
    Car(...),
    Bicycle(...)
  }
Or this (although this is the least common):

  struct Vehicle
  {
    data: VehicleData
    type: Box<dyn SpecificVehicle>
  }

  struct VehicleData
  {
    speed: f32,
  }

  trait SpecificVehicle
  {
    fn quack(&self, data: &VehicleData);
  }

  impl SpecificVehicle for Car {...}
  impl SpecificVehicle for Bicycle {...}