2

The case:

Consider the following:

protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    static var country: String = "Germany"
    var id: Int
    var name: String
}

struct Toyota: Car {
    static var country: String = "Japan"
    var id: Int
    var name: String
}

Here I have a simple example of how to create an abstraction layer by using a -Car- protocol, thus I am able to declare a heterogeneous collection of cars:

let cars: [Car] = [BMW(id: 101, name: "X6"), Toyota(id: 102, name: "Prius")]

And it works fine.

The problem:

I want to be able to evaluate the equality of the cars (by id), example:

cars[0] != cars[1] // true

So, what I tried to do is to let Car to conforms to Equatable protocol:

protocol Car: Equatable { ...

However, I got the "typical" compile-time error:

error: protocol 'Car' can only be used as a generic constraint because it has Self or associated type requirements

I am unable to declare cars: [Car] array anymore. If I am not mistaking, the reason behind it is that Equatable uses Self so it would be considered as homogeneous.

How can I handle this problem? Could Type erasure be a mechanism to resolve it?

Ahmad F
  • 30,560
  • 17
  • 97
  • 143

4 Answers4

3

A possible solution is a protocol extension, instead of an operator it provides an isEqual(to function

protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
    func isEqual(to car : Car) -> Bool
}

extension Car {
    func isEqual(to car : Car) -> Bool {
        return self.id == car.id
    }
}

and use it

cars[0].isEqual(to: cars[1])
vadian
  • 274,689
  • 30
  • 353
  • 361
2

Here is solution using Type Erasure:

protocol Car {
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    var id: Int
    var name: String
}

struct Toyota: Car {
    var id: Int
    var name: String
}

struct AnyCar: Car, Equatable {
    private var carBase: Car

    init(_ car: Car) {
        self.carBase = car
    }

    var id: Int { return self.carBase.id }
    var name: String {
        get { return carBase.name}
        set { carBase.name = newValue }
    }

    public static func ==(lhs: AnyCar, rhs: AnyCar) -> Bool {
        return lhs.carBase.id == rhs.carBase.id
    }
}



let cars: [AnyCar] = [AnyCar(BMW(id: 101, name: "X6")), AnyCar(Toyota(id: 101, name: "Prius"))]

print(cars[0] == cars[1])

Don't know how to implement this with static property. If I figure out, I will edit this answer.

Bohdan Savych
  • 3,310
  • 4
  • 28
  • 47
1

It is possible to override ==:

import UIKit

var str = "Hello, playground"
protocol Car {
    static var country: String { get }
    var id: Int { get }
    var name: String { get set }
}

struct BMW: Car {
    static var country: String = "Germany"
    var id: Int
    var name: String
}

struct Toyota: Car {
    static var country: String = "Japan"
    var id: Int
    var name: String
}

func ==(lhs: Car, rhs: Car) -> Bool {
    return lhs.id == rhs.id
}

BMW(id:0, name:"bmw") == Toyota(id: 0, name: "toyota")
Vyacheslav
  • 26,359
  • 19
  • 112
  • 194
1

Some good solutions to the general problem have already been given – if you just want a way to compare two Car values for equality, then overloading == or defining your own equality method, as shown by @Vyacheslav and @vadian respectively, is a quick and simple way to go. However note that this isn't an actual conformance to Equatable, and therefore won't compose for example with conditional conformances – i.e you won't be able to then compare two [Car] values without defining another equality overload.

The more general solution to the problem, as shown by @BohdanSavych, is to build a wrapper type that provides the conformance to Equatable. This requires more boilerplate, but generally composes better.

It's worth noting that the inability to use protocols with associated types as actual types is just a current limitation of the Swift language – a limitation that will likely be lifted in future versions with generalised existentials.

However it often helps in situations like this to consider whether your data structures can be reorganised to eliminate the need for a protocol to begin with, which can eliminate the associated complexity. Rather than modelling individual manufacturers as separate types – how about modelling a manufacturer as a type, and then have a property of this type on a single Car structure?

For example:

struct Car : Hashable {
  struct ID : Hashable {
    let rawValue: Int
  }
  let id: ID

  struct Manufacturer : Hashable {
    var name: String
    var country: String // may want to consider lifting into a "Country" type
  }
  let manufacturer: Manufacturer
  let name: String
}

extension Car.Manufacturer {
  static let bmw = Car.Manufacturer(name: "BMW", country: "Germany")
  static let toyota = Car.Manufacturer(name: "Toyota", country: "Japan")
}

extension Car {
  static let bmwX6 = Car(
    id: ID(rawValue: 101), manufacturer: .bmw, name: "X6"
  )
  static let toyotaPrius = Car(
    id: ID(rawValue: 102), manufacturer: .toyota, name: "Prius"
  )
}

let cars: [Car] = [.bmwX6, .toyotaPrius]
print(cars[0] != cars[1]) // true

Here we're taking advantage of the automatic Hashable synthesis introduced in SE-0185 for Swift 4.1, which will consider all of Car's stored properties for equality. If you want to refine this to only consider the id, you can provide your own implementation of == and hashValue (just be sure to enforce the invariant that if x.id == y.id, then all the other properties are equal).

Given that the conformance is so easily synthesised, IMO there's no real reason to just conform to Equatable rather than Hashable in this case.

A couple of other noteworthy things in the above example:

  • Using a ID nested structure to represent the id property instead of a plain Int. It doesn't make sense to perform Int operations on such a value (what does it mean to subtract two identifiers?), and you don't want to be able to pass a car identifier to something that for example expects a pizza identifier. By lifting the value into its own strong nested type, we can avoid these issues (Rob Napier has a great talk that uses this exact example).

  • Using convenience static properties for common values. This lets us for example define the manufacturer BMW once and then re-use the value across different car models that they make.

Hamish
  • 78,605
  • 19
  • 187
  • 280