We have written up an RFC on how Core Solidity should support contract composition, and we would like you to read it and push back on it. Comments, objections, concrete counterexamples, alternative encodings and entirely new proposals are all welcome, and we expect the document to change in response to them.
I don’t think the single parent inheritance works for real world solidity code.
Libraries like OpenZeppelin need to provide a grab bag of functionalities, and a given project needs to pick and choose between a mix of them. It’s not an option to have an entire library like this in a single line, or to only allow importing one.
I’m also concerned about the syntax in the docs for mixins. Whatever theoretical underpinnings mixing have, this is not a developer or auditor friendly way to write this. Perhaps there could be a better syntax for this?
Is there a reason for wanting to do away with classic solidity style inheritance?
The syntax will definitely change. We’re actively working on refining the syntax and making it more familiar to Classic Solidity. So the specific syntax in the example should not be considered.
Is there a reason for wanting to do away with classic solidity style inheritance?
To put it shortly: a lot of complexity without enough flexibility.
Inheritance is a very overloaded tool that is currently used to solve multiple orthogonal problems:
- interfaces/polymorphism
- namespacing/encapsulation
- storage layout
- code reuse/extensibility
The issue is that conflating them makes things easy in simple cases but gets unmanageable when the code grows. This is especially evident in contract libraries.
Having your storage layout tied to the linearization order of your contracts makes extending it harder, which is why historically various workarounds were used (storage gaps) and eventually standards like ERC-7201 emerged. The community is basically already choosing composition over inheritance for this use case. The language should follow. In terms of storage layout, the contract is pretty much a struct anyway.
For namespacing we have modules. We’re also adding exports, which will provide encapsulation.
Interfaces are the one place where inheritance makes some sense, which is why we are probably going to keep it in a way. In the new type system they are going to be modeled with type classes instead, but with a bit of syntax sugar this can be made to still look like inheritance.
Extensibility is the aspect that leans the most heavily on inheritance. That’s where multiple inheritance, linearization and function overrides come in. The problem is that this gives you a lot of power but few safeguards. There’s tension between library authors trying to provide simple ways to extend the base implementation and the language trying to be explicit and not allow action at a distance. A good example is the long discussion we had about the override specifiers and when it’s fine to omit them (which then continued throughout several Github issues). The overall conclusion we had from that was that what’s needed a lot of the time is more of a simple mechanism to add to the base implementation than the power to completely alter the call chain the way virtual functions allow you to.
Multiple inheritance in general is much maligned due to the diamond problem. To solve it Solidity mandates all inheritance to follow the virtual inheritance rules known from C++. This is workable, but does not make things simpler for the user.
In general inheritance has a lot of moving parts and is the most complex feature currently in the language. It interacts with other mechanisms in ways that introduce a lot of corner cases. Initialization order, mutability (which can be different in overrides), shadowing, natspec and many other things have to be accounted for. On top of that, behavior in these corner cases is often not strictly specified so there are many bugs and unexpected quirks.
It’s not like we could not have both inheritance and those other features that replace it, but we just don’t think it makes sense to put a huge effort into replicating it faithfully when the feature is this flawed.
So what do we want instead? I’d say that the baseline is a mechanism similar to what you can get in Rust. You have traits (type classes), modules and structs as separate mechanisms and no inheritance. This is also the way Fe does it: Contract composition.
The main concern we still have is whether that’s good enough or if we do need more. For all its warts, inheritance often did make code reuse easy. A good example is how you can get a custom ERC20 token by inheriting from several base classes with very little extra code. In Fe’s composition model you do need a lot more boilerplate. The question is whether that will be an issue in practice. If we can address it by keeping some good aspects of the inheritance model, we will. This thread is an attempt to answer that question.
Thanks for the explanation!
Storage slots depending on inheritance ordering (and needing to reserve space for upgrading) is indeed ugly and a little dangerous. Making this better by default will be good.
I do think that the current inheritance may have its edge case and compiler side complexity, but it’s been very usable app development. I look forward to seeing a more fleshed out syntax on the mixin side to compare.
Here’s some (late) feedback on the proposal:
Example 4
This looks like simply limiting the design to linear single inheritance. It mentions mixins, but not sure what the extra limitations of those are - they all seem to have all the contract features from Classic Solidity: constructors, virtual methods, storage variables.
I agree with @DanielVF that this is not good enough as a reusable composition mechanism. It removes complexity but without giving much in exchange. It does nothing to enable being able to mix and match different base contracts independently of each other. You’re limited to a single base and it has to have everything else based in so it does not sound like a viable mechanism for libraries like OZ.
Also, it seems to remove the explicit virtual/override. Replacing virtual functions with a different mechanism is fine but if they stay I think it’s better to keep user’s controls over which functions can be extended and which can’t.
Example 4B
This one seems to be recreating multiple inheritance, just in a more limited way.
The syntax with import/export is new and more flexible than is, but it’s still a similar relationship. You can have imports in the mixins you import, so the tree can be just as complex. The fact that base members are not automatically brought in reminds me of private inheritance in C++, just with fine-grained control over it, member by member. In fact, it’s similar to visibility modifiers like private or public, just moved to the derived contract.
The main source of complexity reduction seems to be the removal of all the interactions between the base contracts. No virtual methods, no access to constructors and state variables of other mixins and no super means that we don’t have to think about the call chains that come from linearization. But being able to control these interactions was the main advantage between multiple inheritance and composition.
There’s also no virtual inheritance - base contracts are not deduplicated. Again, limiting complexity but lso functionality compared to composition.
Overall, this seems workable to me, but I’m not sure if this is really better than explicit use of composition.
Example 3
These examples do not seem comparable to me so it’s hard to comment on this one. It’s strictly tied to what the solcore prototype can do rather than what Core Solidity would otherwise be able to do without the proposed inheritance flavors. It’s missing structs (to modularize the storage) and interfaces (to modularize the ABI) while it’s pretty clear these features must eventually be a part of the language. This does not give a good picture into how much the new inheritance system is adding on its own and whether we’d still be fine without it.
Modifiers
The modifiers muddle the picture somewhat and make the document longer. I think those would be better split off into a separate proposal. It’s possible to write the Classic Solidity examples without them and they’re not really tied to inheritance.
