Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Rigid, "family tree"-style inheritance as in classical OOP is pretty much garbage. "A cow is a mammal is an animal" is largely useless for the day to day work we do except in extremely well-planned, large and elaborate ontologies -- something you typically only see in highly structured software like windowing systems. It just isn't useful for the majority of our work.

"Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules [1]. You can bolt behaviors and their types onto structures at will. This is how OO should be.

I put together a visual argument on another thread on HN a few weeks ago:

https://imgur.com/a/class-inheritance-vs-traits-oop-isnt-bad...

[1] Though if you want rules on bounds and associated types, you can have them.



> "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules.

Yes-and-no.

Interfaces still participate in inheritance hierarchies (`interface Bar extends Foo`), and that's in a way that prohibits removing/subtracting type members (so interfaces are not in any way a substitute for mixins). Composition (of interfaces) can be used instead of `extends`, but then you lose guarantees of reference-identity - oh, and only reference-types can implement interfaces which makes interfaces impractical for scalars and unusable in a zero-heap-alloc program.

Interface-types can only expose virtual members: no public fields - which seems silly to me because a vtable-like mechanism could be used to allow raw pointer access to fields via interfaces, but I digress: so many of these limitations (or unneeded functionality) are consequences of the JVM/CLR's design decisions which won't change in my lifetime.

Rust-style traits are an overall improvement, yes - but (as far as my limited Rust experience tells me) there's no succinct way to tell the compiler to delegate the implementation of a trait to some composed type: I found myself needing to write an unexpectedly large amount of forwarding methods by hand (so I hope that Rust is better than this and that I was just doing Rust the-completely-wrong-way).

Also, oblig: https://boxbase.org/entries/2020/aug/3/case-against-oop/


How are interfaces with ability to provide default implementations for members (which both C# and Java allow today) not a substitute for mixins?

"Only reference types can implement interfaces" is simply not true in C#. Not only can structs implement them, but they can also be used through the interface without boxing (via generics).


> How are interfaces with ability to provide default implementations for members (which both C# and Java allow today) not a substitute for mixins?

Those default-implementations are only accessible when the object is accessed via that interface; i.e. they aren't accessible as members on the object itself. Furthermore, interfaces (still) only declare (and optionally define) vtable members (i.e. only methods, properties, and events - which are all fundamentally just methods), not fields or any kind of non-static state, whereas IMO mixins should have no limitations and should behave the same as though you copied-and-pasted raw code.


> Those default-implementations are only accessible when the object is accessed via that interface; i.e. they aren't accessible as members on the object itself.

That's true in C# but not in Java, so it's not something intrinsic to the notion of an interface.

> Furthermore, interfaces (still) only declare (and optionally define) vtable members (i.e. only methods, properties, and events - which are all fundamentally just methods), not fields or any kind of non-static state

This is true, but IMO largely irrelevant because get/set accessors are a "good enough" substitute for a field. That there is even a distinction between fields and properties in the first place is a language-specific thing; it doesn't exist in e.g. Eiffel.


Making the class add 2 methods and a field for every place the interface would define a field adds noise. And if you have a class with 20 interfaces, that can be a lot of noise. When you consider that the class itself doesn't actually need to know anything about the field because only the interface uses them, it's just... ugly.


That's more of an issue with Java not having properties as first class language feature. In C#, you don't have to deal with fields at all, because auto-properties do all the same things:

   int Foo { get; set; }


They just cannot access any field in the class itself. Which means... the default implementations fail to actually implement anything but the simplest methods or you need to expose API-private things via getters violating open-closed principle.

(If you merge multiple interfaces, the implementations of the methods have to match. You end up with even more special getters for each one sometimes.)


In C# interface members can be implemented explicitly, which means that: 1) they are not visible on the object itself, only on interface-typed references to it, and 2) implementations of methods from multiple interfaces don't need to match since they live in separate namespaces.

It's true that you can't access private members (not just fields) on `this` from the mixin interface. But explicit implementations of members mean that only someone explicitly downcasting the object will get access to those members, so accidental access is not an issue.


Rust actually allows one to express "family tree" object inheritance quite cleanly via the generic typestate pattern. It isn't "garbage", it totally has its uses. It is however quite antithetical to modularity: the "inheritance hierarchy" can only really be understood as a unit, and "extensibility" for such a hierarchy is not really well defined. Hence why in practice it mostly gets used in cases where the improved static checking made possible by the "typestate" pattern can be helpful, which has remarkably little to do with "OOP" design as generally understood.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: