Search code examples
swiftgenericsprotocolstype-erasure

How can you store an array of protocols with associated types in Swift without using Any?


I'm looking to do something like this:

protocol StateType { }

struct DogState: StateType { }

struct CatState: StateType { }

protocol Action {
    associatedType ST
    func process() -> ST
}

struct DogAction {
    func process() -> DogState { return DogState() }
}

struct CatAction {
    func process() -> CatState { return CatState() }
}

let actions = [DogAction(), CatAction()]

actions.forEach {
    $0.process()
}

I've tried fooling around with type-erasure but I can't get past the issue of not being able to create heterogeneous arrays.

struct AnyActionProtocol<A: Action, ST: StateType>: Action  where A.StateType == ST {
    let action: A

    init(_ action: A) {
        self.action = action
    }

    func process() -> ST {
        return action.process()
    }
}

let dap = AnyActionProtocol<DogAction, DogState>(DogAction())
let cap = AnyActionProtocol<CatAction, CatState>(CatAction())

When I try this, the error I get is Heterogeneous collection literal could only be inferred to [Any] which is something I'm trying to avoid.

Any help would be appreciated!


Solution

  • PATs are usually complex and do not involve an easier solution. I would suggest you come up with some simpler design for your problem. The problem with your method is actually having PAT and nested protocols and trying to make them work together.

    Even if you type erase Action to some type like AnyAction, these two different types DogAction and CatAction again produce different types and you cannot intermix them inside the array. What you can possibly do is have two type erasures, one for Action and other for StateType. When you wrap CatAction or DogAction inside AnyAction both of them would then wrap in type erase state to AnyState.

    Here is one example you could approach this type erasing both the protocols,

    protocol StateType { }
    
    struct DogState: StateType { }
    
    struct CatState: StateType { }
    
    protocol Action {
        associatedtype ST: StateType
        func process() -> ST
    }
    
    struct DogAction: Action {
        func process() -> DogState { return DogState() }
    }
    
    struct CatAction: Action {
        func process() -> CatState { return CatState() }
    }
    
    struct AnyState: StateType {
        let state: StateType
    
        init(_ state: StateType) {
            self.state = state
        }
    }
    
    
    struct AnyAction: Action {
        typealias ST = AnyState
    
        let processor: () -> AnyState
    
        init<T: Action>(_ a: T)  {
            self.processor = {
                return AnyState(a.process())
            }
        }
    
        func process() -> AnyState {
            return processor()
        }
    }
    
    
    let cat = AnyAction(CatAction())
    let dog = AnyAction(DogAction())
    
    let actions = [cat, dog]
    
    actions.forEach { action in
        action.process()
    }
    

    I still think that you should rethink your solution, this can get more complicated as your types increase.