Using Swift 3.0 (I could use Swift 4.0 if that would help me... But I don't think it will) I would like to Type Erase two levels. I what to type erase a protocol having an associatedtype, which conforms to a protocol that in turn itself has an associatedtype. So one could say that I want to type erase nested associatedtypes.
The code below is an extremely simplified version of my code, but it is more clear that way. So what I really want is something like this:
protocol Motor {
var power: Int { get }
}
protocol Vehicle {
associatedType Engine: Motor
var engine: Engine { get }
}
protocol Transportation {
associatedType Transport: Vehicle
var transport: Transport { get }
}
And then I would like to type erase Transportation
and be able to store an array of AnyTransportation
which could have any Vehicle
which in turn could have any Motor
.
So this is a scenario with 3 protocols, where 2 of them have (nested) associatedtypes.
I do not know how to do this. Actually, I do not even know how to solve the even more simple scenario:
We could simplify the original scenario above to a version where we have 2 protocols, where only 1 of them have an associatedtype:
protocol Vehicle {
var speed: Int { get }
}
protocol Transportation {
associatedtype Transport: Vehicle
var transport: Transport { get }
var name: String { get }
}
Then lets say that we have a Bus
conforming to Vehicle
:
struct Bus: Vehicle {
var speed: Int { return 60 }
}
And then we have two different BusLines, RedBusLine
and BlueBusLine
both conforming to Transportation
struct RedBusLine: Transportation {
let transport: Bus
var name = "Red line"
init(transport: Bus = Bus()) {
self.transport = transport
}
}
struct BlueBusLine: Transportation {
let transport: Bus
var name = "Blue line"
init(transport: Bus = Bus()) {
self.transport = transport
}
}
We can then type erase Transportation
using the base and box pattern and classes, as described by bignerdranch here:
final class AnyTransportation<_Transport: Vehicle>: Transportation {
typealias Transport = _Transport
private let box: _AnyTransportationBase<Transport>
init<Concrete: Transportation>(_ concrete: Concrete) where Concrete.Transport == Transport {
box = _AnyTransportationBox(concrete)
}
init(transport: Transport) { fatalError("Use type erasing init instead") }
var transport: Transport { return box.transport }
var name: String { return box.name }
}
final class _AnyTransportationBox<Concrete: Transportation>: _AnyTransportationBase<Concrete.Transport> {
private let concrete: Concrete
init(_ concrete: Concrete) { self.concrete = concrete; super.init() }
required init(transport: Transport) { fatalError("Use type erasing init instead") }
override var transport: Transport { return concrete.transport }
override var name: String {return concrete.name }
}
class _AnyTransportationBase<_Transport: Vehicle> : Transportation {
typealias Transport = _Transport
init() { if type(of: self) == _AnyTransportationBase.self { fatalError("Use Box class") } }
required init(transport: Transport) { fatalError("Use type erasing init instead") }
var transport: Transport { fatalError("abstract") }
var name: String { fatalError("abstract") }
}
We can then put either RedBusLine
or BlueBusLine
in
let busRides: [AnyTransportation<Bus>] = [AnyTransportation(RedBusLine()), AnyTransportation(BlueBusLine())]
busRides.forEach { print($0.name) } // prints "Red line\nBlue line"
In the blog post about type erasure linked to above, what I want is actually a workaround for Homogeneous Requirement
.
Imagine we have another Vehicle
, e.g a Ferry
and a FerryLine
:
struct Ferry: Vehicle {
var speed: Int { return 40 }
}
struct FerryLine: Transportation {
let transport: Ferry = Ferry()
var name = "Ferry line"
}
I guess we want to type erase Vehicle
now? Because we want an array of AnyTransportation<AnyVehicle>
, right?
final class AnyVehicle: Vehicle {
private let box: _AnyVehicleBase
init<Concrete: Vehicle>(_ concrete: Concrete) {
box = _AnyVehicleBox(concrete)
}
var speed: Int { return box.speed }
}
final class _AnyVehicleBox<Concrete: Vehicle>: _AnyVehicleBase {
private let concrete: Concrete
init(_ concrete: Concrete) { self.concrete = concrete; super.init() }
override var speed: Int { return concrete.speed }
}
class _AnyVehicleBase: Vehicle {
init() { if type(of: self) == _AnyVehicleBase.self { fatalError("Use Box class") } }
var speed: Int { fatalError("abstract") }
}
// THIS DOES NOT WORK
let rides: [AnyTransportation<AnyVehicle>] = [AnyTransportation(AnyVehicle(RedBusLine())), AnyTransportation(AnyVehicle(FerryLine()))] // COMPILE ERROR: error: argument type 'RedBusLine' does not conform to expected type 'Vehicle'
Of course this does not work... because AnyTransportation
expects passing in a type conforming to Transportation
, but AnyVehicle
does not conform to it of course.
But I have not been able to figure out a solution for this. Is there any?
[AnyTransportation<AnyVehicle>]
?Below follows only a more detailed explanation of what I want to achieve with the Original Scenario
My original need is to put any Transportation
, having any Vehicle
, that in itself has any Motor
inside the same array:
let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array
If you want to express any transportation with any vehicle with any engine, then you want 3 boxes, each talking in terms of the "previous" type-erased wrappers. You don't want generic placeholders on any of these boxes, as you want to talk in terms of fully heterogenous instances (e.g not any transportation with a specific Vehicle
type, or any vehicle with a specific Motor
type).
Furthermore, rather than using a class hierarchy to perform the type erasing, you can use closures instead, which allows you to capture the base instance rather than storing it directly. This allows you to remove a significant amount of the boilerplate from your original code.
For example:
protocol Motor {
var power: Int { get }
}
protocol Vehicle {
associatedtype Engine : Motor
var engine: Engine { get }
}
protocol Transportation {
associatedtype Transport : Vehicle
var transport: Transport { get }
var name: String { get set }
}
// we need the concrete AnyMotor wrapper, as Motor is not a type that conforms to Motor
// (as protocols don't conform to themselves).
struct AnyMotor : Motor {
// we can store base directly, as Motor has no associated types.
private let base: Motor
// protocol requirement just forwards onto the base.
var power: Int { return base.power }
init(_ base: Motor) {
self.base = base
}
}
struct AnyVehicle : Vehicle {
// we cannot directly store base (as Vehicle has an associated type).
// however we can *capture* base in a closure that returns the value of the property,
// wrapped in its type eraser.
private let _getEngine: () -> AnyMotor
var engine: AnyMotor { return _getEngine() }
init<Base : Vehicle>(_ base: Base) {
self._getEngine = { AnyMotor(base.engine) }
}
}
struct AnyTransportation : Transportation {
private let _getTransport: () -> AnyVehicle
private let _getName: () -> String
private let _setName: (String) -> Void
var transport: AnyVehicle { return _getTransport() }
var name: String {
get { return _getName() }
set { _setName(newValue) }
}
init<Base : Transportation>(_ base: Base) {
// similar pattern as above, just multiple stored closures.
// however in this case, as we have a mutable protocol requirement,
// we first create a mutable copy of base, then have all closures capture
// this mutable variable.
var base = base
self._getTransport = { AnyVehicle(base.transport) }
self._getName = { base.name }
self._setName = { base.name = $0 }
}
}
struct PetrolEngine : Motor {
var power: Int
}
struct Ferry: Vehicle {
var engine = PetrolEngine(power: 100)
}
struct FerryLine: Transportation {
let transport = Ferry()
var name = "Ferry line"
}
var anyTransportation = AnyTransportation(FerryLine())
print(anyTransportation.name) // Ferry line
print(anyTransportation.transport.engine.power) // 100
anyTransportation.name = "Foo bar ferries"
print(anyTransportation.name) // Foo bar ferries
Note that we still built AnyMotor
despite Motor
not having any associated types. This is because protocols don't conform to themselves, so we cannot use Motor
itself to satisfy the Engine
associated type (that requires : Motor
) – we currently have to build a concrete wrapper type for it.