Swift: erasing a nested type
Using Swift 3.0 (I could use Swift 4.0 if it helped me ... But I don't think it will) I would like to type Erase two levels. I, what to type, erases a protocol with an associated type, which corresponds to the protocol, which in turn has an associated type. Thus, it can be said that I want to print the erasure of nested associated types.
The code below is an extremely simplified version of my code, but it is clearer. So what I really want is something like this:
Original Screenplay - Undisclosed
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 AnyTransportation
that anyone can have Vehicle
, which, in turn, anyone can have Motor
.
So this is a scenario with 3 protocols where 2 of them have (nested) related types.
I do not know how to do that. In fact, I don't even know how to solve an even simpler scenario:
Simplified scenario - unresolved
We could simplify the original scenario above to a version where we have 2 protocols where only one of them has an associated type:
protocol Vehicle {
var speed: Int { get }
}
protocol Transportation {
associatedtype Transport: Vehicle
var transport: Transport { get }
var name: String { get }
}
Then let's say what matches Vehicle
:
struct Bus: Vehicle {
var speed: Int { return 60 }
}
And then we have two different BusLines, RedBusLine
and BlueBusLine
both correspondTransportation
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 template 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") }
}
Then we can 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 type erasure blog post linked to above, what I want is actually a workaround for Homogeneous Requirement
.
Suppose we have another one Vehicle
like 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'm assuming we want to type erase Vehicle
now? Since we want an array 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 doesn't work ... because it AnyTransportation
expects to pass in a type that matches Transportation
, but AnyVehicle
doesn't match of course.
But I haven't been able to find a solution for this. Whether there is a?
Question 1: Is it possible to erase a simple script that allows [AnyTransportation<AnyVehicle>]
:?
Question 2: If simple script is allowed, is original script allowed?
The following is just a more detailed explanation of what I want to achieve with the original script
Original Screenplay - Extended
My initial need is to put anyone Transportation
, having any Vehicle
, which by itself has any Motor
within the same array:
let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array
source to share
If you want to express any transportation by any vehicle with any engine, then you need 3 boxes, each of which speaks in terms of "previous" erasable types. You don't want generic placeholders added in each of these fields, as you want to speak in terms of completely heterogeneous instances (for example, not transport with a specific type Vehicle
or any vehicle with a specific type Motor
).
Also, instead of using a class hierarchy to perform type erasure, you can use locks instead, which allows you to grab the underlying instance rather than store it directly. This removes much of the template from the source 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 are still building AnyMotor
, even though Motor
it has no associated types. This is because the protocols do not conform to themselves , so we cannot use Motor
to satisfy the Engine
associated type (which requires : Motor
) - we currently need to create a specific wrapper type for this.
source to share
Hamish's solution is definitely the right way to do what you asked, but when you get into this erasing of styles there are a few questions to ask yourself.
Let's start at the end:
let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array
What can you do with transportations
? Seriously, what kind of code would you write to iterate over it without checking as?
? The only common method available is name
. You could not call anything else, because the types will not match at compile time.
This is really close to the example from my Beyond Crusty , and I think you should look for the same place for solutions. For example, instead of this:
struct RedBusLine: Transportation {
let transport: Bus
var name = "Red line"
init(transport: Bus = Bus()) {
self.transport = transport
}
}
consider solutions that look like this (i.e. no protocols and all PAT problems evaporate):
let redBusLine = Transportation(name: "Red line",
transport: Vehicle(name: "Bus",
motor: Motor(power: 100))
Then consider if you really think that Bus
is a structure. Are two tires with the same properties the same tire?
let red = Bus()
let blue = Bus()
Are the red and blue tires the same? If it is not, then it is not a value type. It is a reference type and should be a class. A lot of Swift's negotiations push us towards protocols and disgrace us by class, but Swift's actual design encourages exactly the opposite. Make sure you avoid activities because these are real value types, not just peer pressure. Don't use protocols just because it's Swift. I believe PAT is a tool for very specialized needs (like Collection) and not for solving most problems. (Before Swift 4, even a collection was a complete mess of protocol.)
source to share
This article might help: https://gist.github.com/dtartaglia/0b5188eaa825b1239389b377d8cb23c1
Erasing two levels in Swift 3
I recently converted my project to Swift 3 ( https://github.com/dtartaglia/XStreamSwift ) and I had to deal with a problem that I couldn't find an answer to. This article focuses on the problem and solution that I found.
source to share