Open divyekapoor opened 10 months ago
Summary:
Requested Outcome: Disengage the conflation of type composition and code composition through the delegation pattern.
Private members are not accessible by subclasses in Neat. (I don't think?) I haven't added protected
yet.
Generally speaking, Neat deemphasizes inheritance to begin with. I think the dangers of inheritance are overstated because people use it for things where it doesn't belong. As a result, I haven't run into any of the issues you speak of, but they seem to me like issues of communication more so than language design.
Also, sumtypes let you upgrade a class by just writing a new BaseclassV2 and putting it in a sumtype with Baseclass in your library. This should significantly reduce the overhead.
I don't follow your point about dependency injection. You should be defining an interface, that both your implementation and the test implementation inherit from.
I'm not planning to make any of those changes, as my experience with OOP doesn't bear them out. If you want to add composition-based features to the language, be my guest, macros are right there - if there's some missing macro feature that prevents parity between macro OOP and builtin OOP please let me know and I'll add it.
Thanks for the response!
Private members are not accessible by subclasses in Neat. (I don't think?) I haven't added protected yet.
Sounds good - maybe consider not adding them at all?
Generally speaking, Neat deemphasizes inheritance to begin with. +1!
I think the dangers of inheritance are overstated because people use it for things where it doesn't belong
Thought experiment: What's a bright line test where inheritance does / does not belong? (The original use for inheritance was for polymorphic dispatch in a language which did not support generics / template types - essentially introducing covariance and contravariance [1] "sneakily" through polymorphic dispatch to more specialized classes and through static binding to base classes - the advent of generics and traits / concepts made this largely obsolete).
It's none of my business, though having no inheritance will probably simplify the variable scope resolution, type checking (the type is the type, it's unique and it's not related to any other type other than through interfaces), type casting (no dynamic_cast issues without vtables/RTTI), any reflection based code (no need for special "getSuperClass()" type methods), the compiler's function binding code (static dispatch only) and you'll guarantee a language without hidden vtable function pointers. Bonus: macro type system will be clean.
Also, sumtypes let you upgrade a class by just writing a new BaseclassV2 and putting it in a sumtype with Baseclass in your library.
Consider what would happen to generics, reflection and macros if this is the path being chosen.
I don't follow your point about dependency injection. You should be defining an interface, that both your implementation and the test implementation inherit from.
Yes - we both agree. However, if Cat inherits from Animal, there's no way to stub out the Animal's code in the test code's construction of Cat - the Cat constructor will hard code the Animal constructor. If the test wants to stub out the Animal implementation, it is disallowed by the language. This isn't a language feature, it's a bug.
(Minor point: Cat inherits from Animal is the same as Cat(Animal a), the 2nd one is dependency injection compatible, the first one has no way to inject an Animal type in the constructor - the closest we can get are:
I'm not planning to make any of those changes, as my experience with OOP doesn't bear them out. If you want to add composition-based features to the language, be my guest, macros are right there - if there's some missing macro feature that prevents parity between macro OOP and builtin OOP please let me know and I'll add it.
Fair enough. Thanks for the option. I'll pass. My motivation to spend time in these threads is to share learnings from hard fought experience. In my daily job I build frameworks where I've had more opportunity than most to see the type system's greatness, edge cases and failings up close. In a sense, I'm a language designer's power user and I've spent tens of thousands of lines of metaprogramming building frameworks used at industrial scale.
Here, I have the opportunity to influence your thinking at the early stages of a promising project - a stage where I believe some of my effort will yield disproportionate long term results. My goal here is to not be a crank. If I'm anywhere close to doling out gray bearded advice in this area, I will have been successful.
In summary, my last pieces of advice are:
I wish you the best! Neat is promising.
Please feel free to close out this ticket.
Cheers! Divye
[1] https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Interfaces
Single class inheritance is functionally identical to a delegation pattern (the hidden *this is made explicit). The problem with inheritance is that it couples state and implementation by breaking encapsulation.
The alternative is to be inheritance free and when inheritance has to be simulated, use a delegation pattern.
Eg. (in pseudocode)
it’s very important to not allow private member leakage as refactors of a widely used base class with non trivial internal state become impossible because of long distance stateful couplings of internal state. I’ve seen this several times in widely used codebases at Google while working on Search and all such refactors of class hierarchies were close to no-go as they were 3 levels of stateful class couplings and no one could predict if a local change in the internals of the base class was safe (and thread safe) because of the internal accesses done by “derived!!! classes”.
Even if the code was completely audited for safety, there could be a PR in flight that relied on the old thread safety assumptions inside the internal code and a silent corruption due to that would be pretty bad.
To make this a bit more concrete - I’m sure you can understand that if a stateful class was implementing a docId counter, such a piece of code would be widely reused with small variations and changing the base class would be an extremely delicate operation.
For reasons like this and others, the modern trend is towards trait delegation rather than traditional inheritance.
Here are 2 blog posts that I think you might find helpful in the context of this issue:
https://www.divye.in/2019/06/modern-programming-never-use.html - OOP inheritance = conflation of type composition and implementation composition in the implementation space (bad! breaks dependency injection).
https://www.divye.in/2022/02/public-static-is-harmful-it-has-no-home.html (slightly off topic, the main link being that this is an example where type-composition and code composition were conflated in the space of types - it's the inverse problem of inheritance).
The only way to avoid both of these traps is to ensure that Types and Implementations can't be coupled. The correct relationship between them is through the delegation pattern as above.
Thanks for taking the time to read this. I realize this is a major change that’s being proposed. I hope you’ll consider it with due care. The best and only time to resolve something like this is early in the evolution of the language. I really liked how Neat is structured. Like you, I seek an alternative to the C++ mess and Rust doesn’t cut it for me.
Thanks and best wishes! Divye