Search code examples
iosswiftswift3type-erasure

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

Solution

  • 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.