[swift-evolution] [Draft] Mixins
Thorsten Seitz
tseitz42 at icloud.com
Tue Mar 1 16:09:02 CST 2016
> Am 29.02.2016 um 19:51 schrieb Антон Жилин via swift-evolution <swift-evolution at swift.org>:
>
> I'm prepairing to create a pull request, so I moved the proposal from Gist to my fork of swift-evolution.
> Link to the proposal new and hopefully final home:
> https://github.com/Anton3/swift-evolution/blob/mixins/proposals/NNNN-mixins.md
I'm -1 to the proposal as written and will try to explain that.
Regarding mixin inheritance:
> Problem of members with same signatures inherited from two different places results in compile error.
This is unfortunate as there exists a simple solution for this problem in statically typed languages (more on that later).
> Diamond problem is solved by keeping only one copy of mixins mixed-in in the final struct/class.
> It works as if bodies of all mixins indirectly mixed-in into D were merged directly into D
That is not a good solution but just a workaround. It probably has origins in dynamically typed languages where a proper solution is not possible (see below).
Unfortunately the current discussions about Mixins, abstract classes, POP vs. OOP suffer from having forgotten achievements of the past which results in creating differences where none should be.
Let's start with protocol oriented programming (POP). This is a term coined by Dave Abrahams in his great talk at WWDC 2015. The only problem IMO is that POP is nothing new. It is just good old interface oriented programming as has been possible in Java for a long time (the new thing Swift brought to the table is first class value semantics). Actually if you look closely at Dave's talk is not about POP vs. OOP it is about Multiple Inheritance Done Right (TM) vs. single inheritance (with a smattering of value objects).
And protocols in Swift have just the same motivation like interfaces in Java (for the moment disregarding value types): they are classes without state because people thought that the dreaded diamond problem is about state and they thought that if dropping state would make multiple inheritance possible.
That's true to some extent. But actually the diamond problem is not about state. State is only where it surfaces more easily.
Actually the diamond problem is about inheriting a member with the same name (or signature) twice. And that is relevant for protocols as well.
And the beauty is that the solution is the same for state as for members.
So, what is the problem? It is as follows:
GENERAL DIAMOND PROBLEM: When a subclass inherits a member with the same name twice we have to know whether both inherited members have the same semantics.
(A) If both have the same semantics then we only want to use one implementation slot, so we either choose a preferred one from the parents or we implement the method in the subclass and are free to call one or both super implementations or replace their logic with something new. What we do is defined in the subclass.
In case of storage we don't have to do anything, the state is merged automatically (i.e. there will be only one storage slot).
(B) If both have different semantics we want to differentiate between both implementations or storage slots. For that we have to rename one or both in the subclass so they don't clash anymore. When using an instance of the subclass with a static type of one of its parents the original name is used of course in the type i.e. at the call site but the correct renamed one is used internally. Which one is clear as that is given by the static type used.
If a (possibly renamed) member has been abstract it remains abstract unless implemented in the subclass. Of course I can still override a member in the subclass even if it was renamed.
The classical diamond problem will in most cases fall into category (A), i.e. both inherited members have the same semantics and therefore only one storage slot ist used. In some cases we might want to duplicate the storage slot but this implies different semantics, so (B) applies and renaming solves this quite nicely, thereby making the different semantics clear by the now different names.
Addendum: in the diamond case we have to `select` one of both members to be used when accessed through the top type.
Bertrand Meyer's Eiffel demonstrates this solution quite nicely and successfully and that's why there are no interfaces in Eiffel, just classes. Because interfaces are not needed if classes use multiple inheritance. Just add abstract methods and you are done. No need to introduce new concepts.
See https://www.eiffel.com/values/multiple-inheritance/
and https://docs.eiffel.com/book/method/et-inheritance#Multiple_inheritance_and_renaming (for the details)
EXAMPLE 1: (from the Eiffel webpages, renaming syntax to be bikeshedded)
class Array {
var count: Int
}
class List {
var count: Int
}
class ArrayList : Array renaming count to capacity, List renaming count to size { ... }
// ArrayList exposes the renamed members only under their new name
let arrayList: ArrayList = ...
arrayList.size // ok, answers the count as inherited from List
arrayList.capacity // ok, answers the count as inherited from Array
arrayList.count // error: undefined `count`
// The original names must still be accessible from the parent types, e.g. Array
let array: Array = arrayList
array.count // ok, answers the count as inherited from Array (= arrayList.capacity)
array.capacity // error: undefined `capacity`
EXAMPLE 2: (from the Eiffel webpages, renaming syntax to be bikeshedded)
class UniversityPerson {
var computerAccount: Account
}
class Teacher : UniversityPerson { ... }
class Student : UniversityPerson { ... }
class TeachingAssistant :
Teacher renaming computerAccount to facultyAccount select facultyAccount,
Student renaming computerAccount to studentAccount {
...
}
let assistant: TeachingAssistant = ...
assistant.facultyAccount // ok
assistant.studentAccount // ok
assistant.computerAccount // error: undefined `computerAccount`
let teacher: Teacher = assistant
teacher.computerAccount // ok (= assistant.facultyAccount)
let student: Student = assistant
student.computerAccount // ok (= assistant.studentAccount)
let universityPerson: UniversityPerson = assistant
universityPerson.computerAccount // ok (= assistant.facultyAccount, due to `select`)
Dynamically typed languages like Ruby cannot use this solution because it is not possible to map a call being made by using a parent type (Array in the example) to the renamed member implementation (Array.count -> ArrayList.capacity) as there are no types and it would not be clear whether array.count would need to call `capacity` or `size`. So renaming is not a possibility in dynamically typed languages which is why they resort to workarounds.
So in essence interfaces or protocols aren't needed at all if multiple inheritance is done right.
Citing from the Eiffel web pages: "Some O-O languages support only the two extremes: fully effective classes, and fully deferred "interfaces", but not classes with a mix of effective and deferred features. This is an unacceptable limitation, negating the object-oriented method's support for a seamless, continuous spectrum from the most abstract to the most concrete."
Now we come back to value types in Swift (which are a great thing!). Why do we need protocols for value objects or rather why is inheritance not allowed for them (because if it was and if multiple inheritance was done right no protocols would be needed)?
AFAIU and I might be very wrong here, inheritance is not allowed for value types because we want to embed value types within other value types (e.g. arrays but other structs as well) without having to use references.
But if a value subtype would have added some state an object of that subtype would require more memory than an object of its parent type which is not possible.
The solution is to forbid inheriting state. And what is commonly used for inheritance without state? Right, interfaces, a.k.a. protocols.
Another solution would have been to just forbid inheriting from a class with state. But that ship has sailed a long time ago, I fear, and protocols add another variety of generics into the language feature mix which offers some nice advantages (while having some disadvantages as well, but that is another topic and there are solutions for that as well).
TL;DR
It is unfortunate and IMO just for historical reasons that there is a dichotomy between protocols and classes at all instead of having just classes with multiple inheritance done right (and abstract methods).
What does that mean for the proposals currently being discussed (Mixins, abstract classes)?
- I think we should NOT add mixins with less than full multiple inheritance.
- We should extend protocols to support real multiple inheritance with renaming
- An open question is whether we should extend protocols to support state and what to do with classes in that case (maybe reduce them to implementation inheritance and having types defined solely by protocols; this would enable classes to use their own namespace different from the namespace of types and get rid of the problem of Collection vs. CollectionImpl or CollectionType vs. Collection).
DREAM
In an ideal world we would just have classes with multiple inheritance and abstract methods and would be able to mark classes as value types which would make them have value semantics like current Swift structs. If subclasses of value types introduce additional state they do not conform to their parent but start a new virtual root in the inheritance tree, i.e.
abstract class Vector<N: Number> {
func +(lhs: Self, rhs: Self) -> Self
func *(lhs: N, rhs: Self) -> Self
}
value class Vec2d<N: Number> : Vector<N> {
var x: N
var y: N
}
value class Vec3d<N: Number> : Vec2d<N> {
var z: N
}
var v2: Vec2d<Float> = ...
var v3: Vec3d<Float> = ...
var v: Vector<Float>
v = v2 // ok
v = v3 // ok
v2 = v3 // type error: Vec3d is not a subtype of Vec2d
For generics I would use both variants that Swift currently supports, i.e. explicit type parameters and associated types, freely combinable.
Just dreaming... sigh.
-Thorsten
More information about the swift-evolution
mailing list