- Status: Active
- Phase: (1/3)Gathering feedback
~Type Hierarchy - an ancestry representing what type is (class inheritance), and what type has (protocol conformance).
Currently swift allows to declare rich protocol hierarchies, which serve the purpose of abstracting building blocks of software, while also providing the minimal dependency requirements between such blocks. Being a language that relies on paradigm of objects and inheritance, swift also provides capabilities to give these interfaces (or protocols in swift' terminology) the default implementation for its members (which makes them essentialy mixins). A teqnique known as retroactive moddeling can easily expand type' inheritance network to be consisting of multitude of entities that each can provide unique implementation of some functionality, without limiting the ability of being a customization point.
protocol Warrior {
func pronounceBattlecry()
}
extension Warrior {
func pronounceBattlecry() {
print("Whoaaaa!!!")
}
}
struct GenericSoldier: Warrior {}
struct Goliath: Warrior {
func pronounceBattlecry() {
print(randomRomanParable())
}
}
Good as it sounds, there is a problemm with this as it implemented in swift.
Any software inevitably grows and some types must provide different implementation for their members, but at some points in a programm, objects that you create may be wished to be used with different default implementation. This problem is related to 'existential types are not extendable' old thread on this site.
Consider example:
let mainStageTonight = Set<MusicPerformer>([FreddieMerqurie(), JimmyHendrix()]) //can store any musician whose performance quality can be compared protocol MusicPerformer: Comparable { var crowdExcitement: Int { Int.random(in: 0..<9) } func singASong() { print("...doing some performance...") } func < (lhs: MusicPerformer, rhs: MusicPerformer) -> Bool { if lhs.crowdExcitement < rhs.crowdExcitement { return true } if lhs.crowdExcitement > rhs.crowdExcitement { return false } } } struct FreddieMerqurie: MusicPerformer, Comparable { func singASong() { print("You have been rocked!") } let crowdExcitement: Int let wasOnDrugs: Bool func < (...) ... } struct JimmyHendrix: MusicPerformer, Comparable { func singASong() { print("Such powerful riffs!") } let crowdExcitement = 10 let whatever: ... func < (...) ... }
This means that being different people, doesnt preclude the ability to be compared as music performers.
There is a requirement to each singer: they must be comparable between each other (while being deferent people/instances of different types), and they also must expose unique singing. In precise words, I want existential view of these instances to be comparable, whilst I also want they to provide unique comparison when they are instances of a concrete type. To rephrase: I want logic for < operator to come from MusicPerformer:Comparable (existential type) and singASong
from ?:MusicPerformer (from concrete type).
After all, FreddieMerquries and JimmyHendrixes can have different measurements when comparing to themselves on the other day.
Now question: Do you expect the following function to work properly?
func pickBestThanSing <C: Collection>(_ a: C) throws where C.Element: MusicPerformer {
print(a.max().singASong() ?? throw "No musicians were wound")
}
Seems fine, right? Nope, it will not print correct answer because the way that swift resolves conformances. Whenever the concrete type' declares new conformance, the default implementation from protocol gets 'lost' (instances of MusicPerformer
will use the implementation from concrete type) and there is no way to recover it, and thus, there is no wau to correctly compare instances which viewed as MusicPerformer
s.
Actually this doesnt just compile, because, u know, error: type MusicPerformer cannot have inheritance clause
.
An advised way to work around this problem today is to use adapters.
struct SomeMusicPerformer: Comparable {
let performer: MusicPerformer
func < (lhs: Self, rhs: Self) -> Bool {} ... }
}
//existential is now materialized
There are obvious limitations to this approach. First, it is not composable: for every combination of two implementation for a single protocol, one adapter is required. If you'd need to compose, say, 3 protocols in a described way, the amount of boilerplate adapter instances is hard to count.
Second, if you need to swap the implementation of how SomeMusicPeformer does comparison you'd need to create yet another wrapper. Likely, it'd be something resembling this:
struct SomeMusicPerformer2: Comparable {
let performer: SomeMusicPerformer
...
}
Third, it's ugly.
Luckily, a better way can be taken.
To amend this plight situation, we (you and me, baby) propose addition of syntactic construction (simmilar to try-catch blocks), that will enable users to shoose a required conformance on the case by case basis, which provides precise control on the semantic of the code. In simpler words, this would give you ability to say 'I want to use this specific implementation within a type' hierarchy'
This change will require to either expand swift type declaration syntax or add new statement. Both are consedered to be rather light in spelling.
The addition can be one of the following:
- Type range
specific-type → protocol-type | class-type
type → specific-type ('..?')?
type → ('?..')? specific-type
type → specific-type '..' specific-type
Set<MusicPerformer & ?..Comparable>
?..Protocol
means witness for Protocol within a type' hierarchy
Protocol..?
means witness from exactly Protocol
protocol A { ... }
struct B: A { reimpl }
func use <T: A>(_ a: T)
use (B() as ?..A) //from B
use (B() as A..?) //from A
//this can be eventually evolved to allow type bounds
class Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car
class Motorcycle extends Vehicle
class Bicycle extends Vehicle
class Tricycle extends Bicycle
// We need to restrict parking to all subtypes of vehicle, above Tricycle
class Parking<T: Vehicle..Bicycle>
- Scoping block
'with' (protocol-name)+ 'from' ((type)+ | '_') { (expression|statement)* }
with Hashable from KitchenItem {
var set = Set<KitchenItem>()
}
//from now on all witness of Hashable
//are from KitchenItem
show (set: set) {$0.hashValue}
//or like this
with Hashable from KitchenItem {
func use <T: KitchenItem>(_ a: T, clos: (T) -> Void)
}
use (Pan()) {$0.hashValue}
//this func uses Hashable always from KitchenItem
//unles overriden
with Hashable from Pan { use (Pan()) {$0.hashValue} }
//Hashable from Pan
//inner blocks overrides outerblocks
with Hashable from KitchenItem {
var set = Set<KitchenItem>([Pan()])
with Hashable from _ {
//do stuff with objects as ?:Hashable
print(set[0].hashValue == Pan().hashValue)
}
//do stuff with objects as KitchenItem:Hashable
}
I think that scoping block thing should be preffered. A few notes:
with _ { ... }
//like a regular code. Basically, discard overrides
with A from _ { ... }
//uses most recent witness for A
//within a type' hierarchy.
//Inherit other overrides from outer block
with A & B from C { ... }
//reroute, when C is existential type
with A from C & D { ... }
//many at once when C & D are concrete types
with A & B from C & D { ... }
//many at once when C & D are concrete types
Old code will be compatible with this change and expected to produce identical results, unless explicetly used in tandem with proposed feature.
This is likely to not break abi, because it is merely an addition to runtime that overrides dispatch to some witnesses (routs to protocol metatype?); this acts as a dispatch proxy and does not modify type' layouts or such. Beign an additive feature, it does affect resilience, since removing this would invalidate semantic of code, written with this.
Additive feature, that cant be removed.
There are a few options.
- Do nothing. Leave current limitation as are, and let compiler pick implementations itself, limiting expressivity and increasing boilerplate.