Bohdan Savych
Bohdan Savych

Reputation: 3447

Array of protocol type

I have checked all answers about this problem on stackoverflow, but still can not figure out how to fix this. My model looks like this

protocol Commandable: Equatable {
    var condition: Condition? {get set}

    func execute() -> SKAction
}

And 3 structs which implement this protocol

struct MoveCommand: Commandable {

    var movingVector: CGVector!

    //MARK: - Commandable

    var condition: Condition?
    func execute() -> SKAction {
       ...
    }
}

extension MoveCommand {
    // MARK:- Equatable

    static func ==(lhs: MoveCommand, rhs: MoveCommand) -> Bool {
        return lhs.movingVector == rhs.movingVector && lhs.condition == rhs.condition
    }
}

struct RotateCommand: Commandable {
    var side: RotationSide!

    // MARK: - Commandable

    var condition: Condition?
    func execute() -> SKAction {
        ...
    }
}

extension RotateCommand {
    // MARK: - Equatable
    static func ==(lhs: RotateCommand, rhs: RotateCommand) -> Bool {
        return lhs.side == rhs.side && lhs.condition == rhs.condition
    }
}

The problems start when I am trying to create third structure which has array of [Commandable]:

struct FunctionCommand: Commandable {
    var commands = [Commandable]()

The compiler output: Protocol 'Commandable' can only be used as a generic constraint because it has Self or associated type requirements. Then i rewrote my struct in this way:

struct FunctionCommand<T : Equatable>: Commandable {
    var commands = [T]()

I resolve this problem but new problem has appeared. Now i can't create FunctionCommand with instances of Rotate and Move command, only with instances of one of them :( :

let f = FunctionCommand(commands: [MoveCommand(movingVector: .zero, condition: nil), 
RotateCommand(side: .left, condition: nil)], condition: nil)

Any Help would be appreciated.

Update: That article helped me to figure out - https://krakendev.io/blog/generic-protocols-and-their-shortcomings

Upvotes: 9

Views: 4128

Answers (3)

Alistra
Alistra

Reputation: 5195

What you need to do is to use type erasure, much like AnyHashable does in the Swift Standard Library.

You can't do:

var a: [Hashable] = [5, "Yo"]
// error: protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements

What you have to do is to use the type-erased type AnyHashable:

var a: [AnyHashable] = [AnyHashable(5), AnyHashable("Yo")]
a[0].hashValue // => shows 5 in a playground

So your solution would be to first split the protocol in smaller parts and promote Equatable to Hashable (to reuse AnyHashable)

protocol Conditionable {
    var condition: Condition? { get set }
}

protocol Executable {
    func execute() -> SKAction
}

protocol Commandable: Hashable, Executable, Conditionable {}

Then create an AnyCommandable struct, like this:

struct AnyCommandable: Commandable, Equatable {
    var exeBase: Executable
    var condBase: Conditionable
    var eqBase: AnyHashable

    init<T: Commandable>(_ commandable: T) where T : Equatable {
        self.condBase = commandable
        self.exeBase = commandable
        self.eqBase = AnyHashable(commandable)
    }

    var condition: Condition? {
        get {
            return condBase.condition
        }
        set {
            condBase.condition = condition
        }
    }

    var hashValue: Int {
        return eqBase.hashValue
    }

    func execute() -> SKAction {
        return exeBase.execute()
    }

    public static func ==(lhs: AnyCommandable, rhs: AnyCommandable) -> Bool {
        return lhs.eqBase == rhs.eqBase
    }
}

And then you can use it like this:

var a = FunctionCommand()
a.commands = [AnyCommandable(MoveCommand()), AnyCommandable(FunctionCommand())]

And you can easily access properties of commands, because AnyCommandable implements Commandable

a.commands[0].condition

You need to remember to now add Hashable and Equatable to all your commands. I used those implementations for testing:

struct MoveCommand: Commandable {

    var movingVector: CGVector!

    var condition: Condition?
    func execute() -> SKAction {
        return SKAction()
    }

    var hashValue: Int {
        return Int(movingVector.dx) * Int(movingVector.dy)
    }

    public static func ==(lhs: MoveCommand, rhs: MoveCommand) -> Bool {
        return lhs.movingVector == rhs.movingVector
    }
}

struct FunctionCommand: Commandable {
    var commands = [AnyCommandable]()

    var condition: Condition?

    func execute() -> SKAction {
        return SKAction.group(commands.map { $0.execute() })
    }

    var hashValue: Int {
        return commands.count
    }

    public static func ==(lhs: FunctionCommand, rhs: FunctionCommand) -> Bool {
        return lhs.commands == rhs.commands
    }
}

Upvotes: 10

Wisors
Wisors

Reputation: 650

I think it can be easily done by introduction of your own CustomEquatable protocol.

protocol Commandable: CustomEquatable {
    var condition: String {get}
}

protocol CustomEquatable {
    func isEqual(to: CustomEquatable) -> Bool
}

Then, you objects have to conform to this protocol and additionally it should conform Equitable as well.

struct MoveCommand: Commandable, Equatable {
    let movingVector: CGRect
    let condition: String

    func isEqual(to: CustomEquatable) -> Bool {
        guard let rhs = to as? MoveCommand else { return false }

        return movingVector == rhs.movingVector && condition == rhs.condition
    }
}

struct RotateCommand: Commandable, Equatable {
    let side: CGFloat
    let condition: String

    func isEqual(to: CustomEquatable) -> Bool {
        guard let rhs = to as? RotateCommand else { return false }

        return side == rhs.side && condition == rhs.condition
    }
}

All you need to do now is connect your CustomEquatable protocol to Swift Equatable through generic extension:

extension Equatable where Self: CustomEquatable {

    static func ==(lhs: Self, rhs: Self) -> Bool {
        return lhs.isEqual(to: rhs)
    }
}

It's not a perfect solution, but now, you can store your objects in a array of protocol objects and use == operator with your objects as well. For example(I simplified objects a little bit):

let move = MoveCommand(movingVector: .zero, condition: "some")
let rotate = RotateCommand(side: 0, condition: "some")

var array = [Commandable]()
array.append(move)
array.append(rotate)  

let equal = (move == MoveCommand(movingVector: .zero, condition: "some"))
let unequal = (move == MoveCommand(movingVector: .zero, condition: "other"))
let unequal = (move == rotate) // can't do this, compare different types

PS. Using var on struct is not a good practice, especially for performance reasons.

Upvotes: 4

Cenny
Cenny

Reputation: 1984

I believe the problem here is that the equatable protocol has self requirements. So you can solve you problem by removing equatable protocol from your Commandable protocol and make your your structs equatable instead. This will of course limit your protocol but maybe it is a trade-off that is reasonable?

Upvotes: 0

Related Questions