4 ms·
I love how rust does interfaces (traits) and allows implementing outside the type. It feels much more natural to create a subsystem on the side without tangling
by alkonaut 6y ago
I love how rust does interfaces (traits) and allows implementing outside the type. It feels much more natural to create a subsystem on the side without tangling it into other subsystems. E.g. to create a separate rendering subsystem for a game entity you simply
impl Drawable for MyEntity
void Draw(MyEntity self, Canvas...
Which feels quite natural compared to anemic-entity ECS or naive Composition-over-inheritance like so:
class MyEntity {
EntityDrawingComponent drawer
EntityMovingComponent mover;
and of course, over the standard Java/C# OO way
class MyEntity : Drawable, Movable, ...
void Move(Vec..)
void Draw(Canvas..)
With this type of declaration, I'm forced to mix the code for my different subsystems making it not just likely but inevitable that someone eventually ties these subsystems together.
In general, if I were to design a "better" OO language now; i'd make sure to make it almost not OO at all. I'd allow subclass polymorphism, but no virtual methods or classes. Only interfaces/traits and abstract classes. I'd very clearly separate data types from identity types at the language level (which aren't just a difference betweeen stack and heap as with C# structs and classes).
I'd want the bare minimum of functional niceness: enumerations and pattern matching which check for exhaustion. Having that in a lanugage easily lets the developer use different implementation for two completely different cases: open and closed polymorphism sets. OO is great when you don't know what variants might exist, but it's useless when you DO want to control it.
If I as a developer know that the set of Payment options are Credit/Cash/Invoice - then I want it exhaustively checked. I deliberatel do not want to leave that open. I want to be able to switch on the closed set of 3 cases, and be warned if I fail to check a case. I want to be able to do this without inverting the logic like and calling into the unknown like paymentMethod.HandlePayment(order).
- deleted 6y ago[deleted]
- lukevp 6y agoI wonder if you could implement this all within a linter on top of Rust or some other language? Some of your asks already exist at that level (for example, TypeScript autocomplete in VS Code knows which enums you haven’t checked in a switch, and it also knows if a property has been verified that you’ve narrowed via a type union, so perhaps you could extend this to enforce coverage of switch statements?
- bombela 6y agoThe Rust compiler already enforces that you match all variants of an enum. And Rust Enum is basically a C union with an integer tag.
- magicalhippo 6y agoI've only briefly looked at Rust. How would you handle your Drawable if it needs to store some extra data? Few of the things I use inheritance or composition for can be implemented without storing some extra data. As an example, say a buffered Stream, that takes a Stream instance and adds buffered access. This BufferedStream would need to store the buffer data somewhere.
- hderms 6y agoRust has BufWriter/BufReader and I'm not an expert but I think you'd just view it as extension through composition
- magicalhippo 6y agoThat does indeed seem[1] to do regular, naive as per OP, composition. I was just curious what OP was using these traits for, as in my non-Rust experience seldom have the need to compose stuff without storing additional data. [1]: https://doc.rust-lang.org/src/std/io/buffered/bufwriter.rs.html https://doc.rust-lang.org/src/std/io/buffered/bufwriter.rs.h...
- coolreader18 6y agoDrawable is just a trait, a definition of methods that can be abstracted over. If you wanted to store data, you'd do it in the type that Drawable is actually being implemented for, i.e. MyEntity. struct MyEntity { cache: RenderingCache, } impl Drawable for MyEntity { fn draw(&self, canvas: &mut Canvas) { self.cache.draw_cached(canvas, |cache_canvas| { cache_canvas.render(ASSET); }); } } const ASSET: Asset = load_asset("foo.png");
- magicalhippo 6y agoThanks, that is very clear. Think Rust differs enough that it's a bit hard for me to draw direct comparisons with my primary languages, but with the emphasis on interfaces like that it does seem like a nicer way to implement extension methods.
- branko_d 6y ago> stack and heap as with C# structs and classes Nitpick: C# struct can be on the heap (when it's "boxed"). The fundamental difference between C# class and struct is how they treat assignment operation. The class has "reference semantics" (assignment creates a new reference to the same object) and struct has "value semantics" (assignment creates a new object). C# uses terms "reference types" (of which "class" is one) and "value types" (of which "struct" is one) to make the difference clear. Structs being on the stack (most of the time) is just a consequence of their value semantics, and should be treated mostly as an implementation detail.
- alkonaut 6y agoI’m aware of the differences between structs and classes, boxing etc. (.net dev since ‘02), . My point is “class” can some times be a faceless “value” with no identity, and some times have an identity. The “records” feature is a way of addressing this difference, but like everything else it’s tacked on a bit late. A value class, like a value type (struct, enum), should/could be immutable, have structural equality etc. I’d like my language to be extremely explicit about whether an object has identity or not. It ties into things like ownership, move vs. copy, disposal etc too.
- jayd16 6y agoI think you misunderstand the ECS pattern. You want: class DrawSystem{ Draw(DrawComponent) } With some mapping of DrawComponent to MyEntity. That mapping would most likely NOT be in the MyEntity class definition. Or you can put the DrawComponents on the Draw system and simply track mapping back to the MyEntity instance they correspond to. New code lives in the systems. In a game you'd have some kind of scene system that could track what entity has what components and then you don't even need a MyEntity class at all. You wouldn't track that composition in a class. The sugar that Rust is bringing is that it feels like the implementation is being added to the instance instead of an external system outside it, but you can make that work in C# with some extension methods. It would be nicer if/when C# gets extension interfaces.
- alkonaut 6y agoThats exactly how I understand an EC system but my issue with it is the overly soft coupling with just ids.
- jayd16 6y agoI guess you want to know that every MyEntity can be drawn and can move so you don't have to try-get the component. The disconnect I think is that part of the ECS design is that you have the freedom to break that guarantee. Entities are no longer types but collections of components. The soft coupling is the goal.
- alkonaut 6y agoYes exactly, I think it’s an elegant system for some contexts but I wouldn’t want every system to be forced to use it, so I wouldn’t want that ECS design to be a fundamental part of the language. Soft coupling is basically just giving up typing at some interface and relying on soft information. Which can be fine, but not always.
- the_gipsy 6y ago> i'd make sure to make it almost not OO at all. I think you mean "class oriented"
- alkonaut 6y agoNever heard the term, but that could be what it is.
- dirkt 6y ago> I love how rust does interfaces (traits) and allows implementing outside the type. Then it might be interesting for you that Rust copied this (like a few other things) from Haskell, where they are (somewhat confusingly) called type classes.