Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Nested type erasure

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:

Original Scenario - Unsolved

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:

Simplified Scenario - Unsolved

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?

Question 1: Is it possible to type erase the Simple Scenario allowing for: [AnyTransportation<AnyVehicle>]?

Question 2: If the Simple Scenario is solvable, is the original scenario also solvable?

Below follows only a more detailed explanation of what I want to achieve with the Original Scenario

Original Scenario - Expanded

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
like image 937
Sajjon Avatar asked Jul 08 '17 15:07

Sajjon


2 Answers

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.

like image 183
Hamish Avatar answered Nov 04 '22 19:11

Hamish


Hamish's solution is definitely the correct way to do what you've asked, but when you get into this much type erasure, you need to ask yourself some questions.

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 possibly do with transportations? Seriously, what code would you write that iterated over it without doing as? checking? The only general method available is name. You couldn't really call anything else because the types would mismatch at compile time.

This is really close to the example from my Beyond Crusty talk, and I think you should be looking to the same place for solutions. For example, rather than 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 the PAT problems evaporate):

let redBusLine = Transportation(name: "Red line",
                                transport: Vehicle(name: "Bus", 
                                                   motor: Motor(power: 100))

Next, think really hard about whether you mean Bus to be a struct. Are two busses with the same properties the same bus?

let red = Bus()
let blue = Bus()

Are red and blue the same bus? If they're not, then this is not a value type. This is a reference type and should be a class. A lot of Swift talks push us towards protocols and shame us about classes, but the actual design of Swift encourages exactly the opposite. Make sure you're avoiding classes because these are real value types, and not just out of peer pressure. Don't use protocols just because it's Swift. I find PATs to be a tool for very specialized needs (like Collection), not a go-to solution for most problems. (Until Swift 4, even Collection was a total mess of a protocol.)

like image 40
Rob Napier Avatar answered Nov 04 '22 20:11

Rob Napier