Rust and Swift (xv)
Inheritance: a Swiftian specialty (for now).
I am reading through the Swift book, and comparing it to Rust, which I have also been learning over the past few months. As with the other posts in this series, these are off-the-cuff impressions, which may be inaccurate in various ways. I’d be happy to hear feedback! Note, too, that my preferences are just that: preferences. Your tastes may differ from mine. (See all parts in the series.)
The next chapter in the Swift book focuses on inheritance, a concept which does not yet exist in Rust.
Swift embraces classical inheritance for class
data types. As noted previously, Rust’s struct
covers much of the ground covered by Swift’s struct
and class
types together (value and reference types, etc.). However, what Swift’s class
types bring to the table is inheritance-based (and not just composition-based) extension of types.
This is a bit of an interesting point: it is an area where, as of today, Swift can do something that is flat impossible in Rust—a rarity.
However, the status quo will be changing sometime in the next year or so, as there is a Rust RFC which has been accepted and is in the process of being implemented which paves the way for inheritance. (Discussions are ongoing as to the best way to implement it for Rust. Classical inheritance with vtables as in Swift is probably not going to be the approach.)
The reason Rust’s core team chose to proceed without inheritance for the 1.0 release of the language last May is simple: at a philosophical level, they prefer (as in general most developers increasingly acknowledge that we should all prefer) composition over inheritance. Prefer, not universally choose, because there are situations in which inheritance is the correct choice. But there is a reason that programming with interfaces rather than via sub-classing is a “best practice” for many scenarios in languages like Java or C#.
Rust’s trait
system gives you composition in some remarkably powerful ways, allowing you to do things that in C++, for example, have to be accomplished via a combination of inheritance and overloading. Swift, likewise, supplies a protocol
system and allows extensions to define further behavior on top of existing data structures. From what I’ve gathered, those approaches are preferred over inheritance in Swift for the same reason Rust shipped 1.0 without it!
But Swift does have inheritance, so it’s worth seeing how it works.
First, any class
which doesn’t declare a parent from which to inherit is a base class. This is an important difference from, say, Python, where all classes inherit from Object
(leaving aside custom metaclasses).
The syntax choices Swift has made around sub-class declarations are sensible and readable: class SubClass: ParentClass
is eminently readable and doesn’t have any obvious points of overlap with other elements in the language.
Indeed, many of the choices made around classes are quite sensible. Overrides, for example, are made explicit via the override
keyword. While I’ve sometimes poked fun at Swift’s tendency to add keywords everywhere, this seems like a reasonable place to have one, and it’s nice that overrides are explicit rather than implicit. The same is true of the use of super
to refer to the superclass. I’m not sure of the implementation details, but super
appears to act as just a special/reserved name for an object: all the syntax around it is normal object instance syntax, which is as it should be.
The limitations around overriding properties all make sense. You can override a read- or write-only parent property as both readable and writable, but you can’t override a readable or writable property not to be readable or writable respectively. Presumably this is because the method lookup for properties always checks up the inheritance chain for getters or setters, so if one is present, you can’t just get rid of it. (You could of course override with a no-op function that spews a warning or some such, but that would pretty clearly be an abuse of the parent API. There might be times you would do that with a third-party library parent class, but in your own code it should be avoided: it indicates a problem in your API design that you need to address instead.)
Finally, we have Swift’s final
keyword—and yes, pun intended. It marks whatever block-level item it is attached to—whether class, method, or property—as non-overridable. Attempts to override an item marked final are compile-time failures. (The same kind of thing exists in Java and C#.) In and of itself, this isn’t especially interesting. It is interesting to ponder whether you should make classes subclass-able or not in your API design. There has been an active debate, in fact, whether classes in Swift should become final by default in Swift 3.0, rather than open by default. The debate centers on the danger of unintended consequences of overriding, which ultimately takes us back around to the preference for composition, of course.
All of this, among other things, raises the very interesting question of what this will look like in Rust when, eventually, we get inheritance there. After all, we know it will be quite different in some ways:
It presumably won’t involve a distinct data type constructor, a la Swift’s distinction between
struct
andclass
: there may be syntactic sugar involved, and there will definitely be new functionality present, but it will certainly be built on the existing language features as well. There’s a good chance it will basically look like just a special case ofimpl SomeTrait for SomeStruct
, which would fit very well with the ways Rust solves so many other problems.Rust doesn’t have many of the things which Swift takes care to special-case for overriding with
final
, but it will need to address that case for inherited methods and data in some way. (The proposal linked above uses a distinction betweendefault
and blanket implementations for trait specialization to pull this off; if those words don’t mean anything to you, don’t worry: I’ve read that post and RFC half a dozen times before I got a really solid handle on all the pieces involved.)It will be a relative latecomer to the language, rather than baked in from the start, and therefore will likely seem a secondary way of solving problems, especially at first. (This is, I think, both intentional and good.)