4
votes

I am trying to make a simple game implementation. So each game has a correct Answer. The Answer could be an Int or String. So what I have in code is:

protocol Answer {}
extension Int: Answer {}
extension String: Answer {}

protocol CorrectAnswer {
    var correctAnswer: Answer { get }
}

I have a protocol for what a game needs:

protocol GameDescriber {
    var name: String { get }
    var description: String { get }
    var points: Int { get }
}

And the implementation of the Game struct:

struct Game: GameDescriber, Equatable, CorrectAnswer {
    var correctAnswer: Answer
    var name: String
    var description: String
    var points: Int

    static func ==(_ lhs: Game, _ rhs:Game) -> Bool {
        if let _ = lhs.correctAnswer as? String, let _ = rhs.correctAnswer as? Int {
            return false
        }

        if let _ = lhs.correctAnswer as? Int, let _ = rhs.correctAnswer as? String {
            return false
        }

        if let lhsInt = lhs.correctAnswer as? Int, let rhsInt = rhs.correctAnswer as? Int {
            if lhsInt != rhsInt {
                return false
            }
        }

        if let lhsString = lhs.correctAnswer as? String, let rhsString = rhs.correctAnswer as? String {
            if lhsString != rhsString {
                return false
            }
        }

        return lhs.description == rhs.description &&
            lhs.name == rhs.name &&
            lhs.points == rhs.points
    }
}

If I want to add another Answer type (let's say an array of Ints) I have to do that:

extension Array: Answer where Element == Int {}

But what bothers me is in the implementation of the Equatable func == I have to cover this and possibly other cases as well. Which can me dramatic :)

Is there a solution for this and can it be done in more elegant and generic way?

1

1 Answers

1
votes

First note that your implementation of == can be simplified to

static func ==(_ lhs: Game, _ rhs:Game) -> Bool {
    switch (lhs.correctAnswer, rhs.correctAnswer) {
    case (let lhsInt as Int, let rhsInt as Int):
        if lhsInt != rhsInt {
            return false
        }
    case (let lhsString as String, let rhsString as String):
        if lhsString != rhsString {
            return false
        }
    default:
        return false
    }
    return lhs.description == rhs.description &&
        lhs.name == rhs.name &&
        lhs.points == rhs.points
}

so that adding another answer type just means adding one additional case.

The problem is that the compiler cannot verify that all possible answer types are handled in your == function, so this approach is error-prone.

What I actually would do is to use an enum Answer instead of a protocol, and make that Equatable:

enum Answer: Equatable {
    case int(Int)
    case string(String)
}

Note that you don't have to implement ==. As of Swift 4.1, the compiler synthesizes that automatically, see SE-0185 Synthesizing Equatable and Hashable conformance.

And now Game simplifies to

struct Game: GameDescriber, Equatable, CorrectAnswer {
    var correctAnswer: Answer
    var name: String
    var description: String
    var points: Int
}

where the compiler synthesizes == as well, with a default implementation that compares all stored properties for equality.

Adding another answer type is simply done by adding another case to the enumeration:

enum Answer: Equatable {
    case int(Int)
    case string(String)
    case intArray([Int])
}

without any additional code.