Is it a good idea to implement the Equatable protocol for a Swift struct in a way to compare only ID-s (which is supposed to be unique) and ignore the rest of the properties? What are drawbacks of this approach?
struct Person {
let id: String
let name: String
var surname: String
}
extension Person: Equatable {
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
}
Related
I have a protocol that represent brand for example
I have two class in this example, Jbl and Columbo that conform to Brand protocol
The builder build some stuff and finally return the name of Jbl or Columbo class
(The build stuff is simplified with random in code sample)
In this sample code we can expect randomly Jbl Brand or Columbo Brand in the print result
But this cannot compile with error : Protocol 'Brand' can only be used as a generic constraint because it has Self or associated type requirements
protocol Brand: AnyObject, Hashable {
var name: String { get }
}
extension Brand {
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.name == rhs.name
}
}
class Jbl: Brand {
var name: String { "Jbl" }
}
class Columbo: Brand {
var name: String { "Columbo" }
}
class Builder {
func build() -> Brand { // Protocol 'Brand' can only be used as a generic constraint because it has Self or associated type requirements
if .random() {
return Jbl()
} else {
return Columbo()
}
}
}
var builder = Builder()
print(builder.build().name)
The problem is that for build() you are trying to return a type of Brand, but Hashable has Self requirements as part of its definition. This is why you get the error about "Self or associated type requirements".
A possible solution is to make Brand not Hashable itself, but any class which conforms to Brand and Hashable will get the Hashable implementation for Brand:
protocol Brand: AnyObject {
var name: String { get }
}
extension Brand where Self: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.name == rhs.name
}
}
However, accessing build() will not give you a hashValue, since only classes which conform to Brand and Hashable have hashValue.
To fix this, conform Jbl and Columbo to the Hashable protocol. Then add a function like this in the Builder class for example:
func getHashValue() -> Int {
if .random() {
return Jbl().hashValue
} else {
return Columbo().hashValue
}
}
Called like:
print(builder.getHashValue())
// Prints: -8479619369585612153
Obviously this hash changes each time the app is relaunched, so don't rely on it for persisting data.
And just for style reasons: you may prefer to use the following and make the classes just conform to HashableBrand:
typealias HashableBrand = Brand & Hashable
I have written a simple protocol Data :
public protocol Data {
var state: [String: Any] { get set }
var objectId: String? { get set }
}
public extension Data {
var objectId: String? {
get {
return self.state["objectId"] as? String
}
set {
self.state["objectId"] = newValue
}
}
}
This way, I have created several types conforming to it:
public struct Person: Data {
public var state: [String : Any]
}
public struct Car: Data {
public var state: [String : Any]
}
// ...
Now what I want to do is to make each of these types Hashable, the problem is that I need to write this code in every type:
extension Car Hashable {
public static func == (lhs: Car, rhs: Car) -> Bool {
return lhs.objectId == rhs.objectId
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.objectId ?? "")
}
}
// ...
What I want to know is if it is possible to generically declare Data as Hashable from its objectId. As I am using a protocol, I couldn't find a way to do so.
Thank you for your help.
As #Leo Dabus mentioned in his comment, you should probably use another name for your protocol due to native Foundation.Data type existence.
Either way, using the following code you can implement Hashable protocol into your Data protocol:
public protocol Data: Hashable {
var state: [String: Any] { get set }
var objectId: String? { get set }
}
public extension Data {
var objectId: String? {
get {
return self.state["objectId"] as? String
}
set {
self.state["objectId"] = newValue
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.objectId == rhs.objectId
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.objectId ?? "")
}
}
Although this will have as a side effect that protocol Data can only be used as a generic constraint because it has Self or associated type requirements:
Meaning that you can now on use the Data protocol like this:
func myFunc<T: Data>(data: T) {
}
I have a protocol:
protocol CustomProtocol {
var title: String { get }
var subtitle: String { get }
}
Then i have 2 objects, that conform this procotol. And i want to compare them, so i would like to CustomProtocol to be Equatable.
protocol CustomProtocol: Equatable {
var title: String { get }
var subtitle: String { get }
static func ==(lhs: Self, rhs: Self) -> Bool
}
extension CustomProtocol {
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.title == rhs.title
}
}
But after that change i get "Protocol CustomProtocol can only be used as a generic constraint because it has Self or associated type requeriments.
The only way i can think to solve this is to have a third property like a hash that depends on the others and compare this property.
Here you have a sample playground with the actual code.
Because Equatable has Self requirements, it should not be implemented directly on a protocol. Otherwise, the protocol will be unusable as a type.
To implement Equatable at the protocol level yet be able to use the protocol as a type, you can use type erasure.
To demonstrate, I have modified the code given in your playground to build a type eraser.
For a detailed explanation of the approach I have used, check out this post on my blog:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-two/
Here is the modified code from your playground:
protocol CustomProtocol {
var title: String { get }
var subtitle: String { get }
func isEqualTo(_ other: CustomProtocol) -> Bool
func asEquatable() -> AnyEquatableCustomProtocol
}
extension CustomProtocol where Self: Equatable {
func isEqualTo(_ other: CustomProtocol) -> Bool {
guard let o = other as? Self else { return false }
return self == o
}
func asEquatable() -> AnyEquatableCustomProtocol {
return AnyEquatableCustomProtocol(self)
}
}
struct A: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: A, rhs: A) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct B: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: B, rhs: B) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct AnyEquatableCustomProtocol: CustomProtocol, Equatable {
var title: String { return value.title }
var subtitle: String { return value.subtitle }
init(_ value: CustomProtocol) { self.value = value }
private let value: CustomProtocol
static func ==(lhs: AnyEquatableCustomProtocol, rhs: AnyEquatableCustomProtocol) -> Bool {
return lhs.value.isEqualTo(rhs.value)
}
}
// instances typed as the protocol
let a: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let b: CustomProtocol = B(title: "First title", subtitle: "First subtitle")
let equalA: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let unequalA: CustomProtocol = A(title: "Second title", subtitle: "Second subtitle")
// equality tests
print(a.asEquatable() == b.asEquatable()) // prints false
print(a.asEquatable() == equalA.asEquatable()) // prints true
print(a.asEquatable() == unequalA.asEquatable()) // prints false
The point to note is that with this approach the actual == comparisons are delegated to the underlying concrete types, yet we deal only with protocol types to maintain abstraction.
Here I have used the type erased instances only for one comparison. However, since the type eraser conforms to CustomProtocol these instances can be saved and used in any place where a protocol type is expected. Because they conform to Equatable, they can also be used in any place where Equatable conformance is required.
Just for context, this post explains why it is not advisable to try to implement Equatable conformance or even == functions directly on protocols:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-one/
Hence the type erasure.
Hope this helps.
The Equatable protocol has a self constraint to solve the problem that you only should be able to check equality between objects of the same type, not the same protocol. That's why it has a self-requirement. Otherwise you could just say
let a: Equatable = 42
let b: Equatable = "hello"
and a == b would work. This would be bad because you could compare objects of totally unrelated types. The self-requirement makes this a compile time error.
If you want to compare your objects on a protocol basis, just implement the == operator without a self-requirement:
extension CustomProtocol {
func == (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
return lhs.name == rhs.name
}
func != (lhs: CustomProtocol, rhs: CustomProtocol) -> Bool {
return !(lhs == rhs)
}
}
Now you can declare instances of your protocol directly with the CustomProtocol type and compare them.
But maybe the protocol is not the right abstraction in this case. Maybe you should implement this as an abstract class.
The problem is the rhs parameter is not the same type of the lhs. They just conform to the same protocol.
You could solve that by using a generic type as the second parameter.
exension CustomProtocol {
static func ==<T: CustomProtocol>(lhs: Self, rhs: T) -> Bool {
return lhs.title == rhs.title
}
}
Consider this example:
protocol Observable: Hashable {
// ...
}
struct People: Observable {
var name: String
var age: Double
var hashValue: Int {
// ...
}
static func ==(lhs: People, rhs: People) -> Bool {
// ,,,
}
}
struct Color: Observable {
var red: Double, green: Double, blue: Double
var hashValue: Int {
// ...
}
static func ==(lhs: Color, rhs: Color) -> Bool {
// ...
}
}
var observers: Set<Observable> = [] // Not allowed by the compiler
People and Color are both conform to Observable protocol which also inherit from Hashable protocol. I want to store these inside the observers set.
using 'Observable' as a concrete type conforming to protocol
'Hashable' is not supported
Is it possible to do heterogenous Set in Swift?
There is a way to make it possible. (Inspired by Apple's implementation)
Before we begin, this is what we want to build.
protocol Observer: Hashable {
associatedtype Sender: Observable
func valueDidChangeInSender(_ sender: Sender, keypath: String, newValue: Any)
}
The source of this problem is the use of Self that force the array to be Homogenous. You can see it here:
The most important change is that it stop the protocol from being usable as a type.
That makes us can't do:
var observers: [Observer] = [] // Observer is not usable as a type.
Therefore, we need another way to make it work.
We don't do
var observers: [AnyHashable] = []
Because AnyHashable will not constrain the object to conform Observer protocol. Instead, we can wrap the Observer object in the AnyObserver wrapper like this:
var observers: [AnyObserver] = []
observers.append(AnyObserver(yourObject))
This will make sure the value of AnyObserver struct conforms to Observer protocol.
According to WWDC 2015: Protocol-Oriented Programming in Swift, we can make a bridge with isEqual(_:) method so we can compare two Any. This way the object doesn't have to conform to Equatable Protocol.
protocol AnyObserverBox {
var hashValue: Int { get }
var base: Any { get }
func unbox<T: Hashable>() -> T
func isEqual(to other: AnyObserverBox) -> Bool
}
After that, we make the box that conforms to AnyObserverBox.
struct HashableBox<Base: Hashable>: AnyObserverBox {
let _base: Base
init(_ base: Base) {
_base = base
}
var base: Any {
return _base
}
var hashValue: Int {
return _base.hashValue
}
func unbox<T: Hashable>() -> T {
return (self as AnyObserverBox as! HashableBox<T>)._base
}
func isEqual(to other: AnyObserverBox) -> Bool {
return _base == other.unbox()
}
}
This box contains the actual value of the AnyObserver that we will create later.
Finally we make the AnyObserver.
struct AnyObserver {
private var box: AnyObserverBox
public var base: Any {
return box.base
}
public init<T>(_ base: T) where T: Observer {
box = HashableBox<T>(base)
}
}
extension AnyObserver: Hashable {
static func ==(lhs: AnyObserver, rhs: AnyObserver) -> Bool {
// Hey! We can do a comparison without Equatable protocol.
return lhs.box.isEqual(to: rhs.box)
}
var hashValue: Int {
return box.hashValue
}
}
With all of that in place, we can do:
var observers: [AnyObserver] = []
observers.append(AnyObserver(yourObject))
Actually, you cannot declare a Set -or even an array- of type Observable, that's because at some level Observable represents a generic protocol:
Observable -> Hashable -> Equatable:
which contains:
public static func ==(lhs: Self, rhs: Self) -> Bool
That's the reason why of the inability of using it in Heterogenous way. Furthermore, you can't declare an existential type:
var object: Observable?
// error: protocol 'Observable' can only be used as a generic constraint
// because it has Self or associated type requirements
If you are wondering what's the reason of this constraint, I assume that it is logical to compare tow People or tow Color, but not comparing People with Color.
So, what can we do?
As a workaround, you could let your set to be a set of AnyHashable structure (as #Leo mentioned in the comment):
The AnyHashable type forwards equality comparisons and hashing
operations to an underlying hashable value, hiding its specific
underlying type.
As follows:
let people = People(name: "name", age: 101)
let color = Color(red: 101, green: 101, blue: 101)
var observers: Set<AnyHashable> = []
observers.insert(people)
observers.insert(color)
for (index, element) in observers.enumerated() {
if element is People {
print("\(index): people")
}
}
that would be legal.
I have a struct in Swift that looks like this:
internal struct MapKey {
internal let id: String
internal let values: [String:String]
}
extension MapKey: Equatable {}
func ==(lhs: MapKey, rhs: MapKey) -> Bool {
return lhs.id == rhs.id && lhs.values == rhs.values
}
I now have the need to use MapKey as the key in a Swift dictionary, which requires MapKey to conform to the Hashable protocol.
What would a correct implementation of Hashable be for a struct like this one?
extension MapKey: Hashable {
var hashValue: Int {
return ??? // values does not have a hash function/property.
}
}
I've been doing some research but failed to identify what the proper way to hash a dictionary is, as I need to be able to generate a hash value for values property itself. Any help is much appreciated.
I think you need to review your data model if you have to use a whole struct as a dictionary key. Anyhow, here's one way to do it:
internal struct MapKey: Hashable {
internal let id: String
internal let values: [String:String]
var hashValue: Int {
get {
var hashString = self.id + ";"
for key in values.keys.sort() {
hashString += key + ";" + values[key]!
}
return hashString.hashValue
}
}
}
func ==(lhs: MapKey, rhs: MapKey) -> Bool {
return lhs.id == rhs.id && lhs.values == rhs.values
}
This assumes that you don't have semicolon (;) in id or in the keys and values of values. Hasable implies Equatable so you don't need to declare it conforming to Equatable again.
Since both id and values are immutable both are ok to use as basis for equals and hashValue.
However - if MapKey.id (which the name somewhat implies) uniquely identifies the MapKey (at least within the context of one dictionary)
then it is both easier and more performant to just use the MakKey.id as basis for == operator as well as hashValue
internal struct MapKey: Hashable {
internal let id: String
internal let values: [String:String]
var hashValue: Int {
get { return self.id.hashValue}
}
}
func ==(lhs: MapKey, rhs: MapKey) -> Bool {
return lhs.id == rhs.id
}
foundation data types are Hashable in Swift 4.2, you only need to let your MapKey struct to conform Hashable protocol:
struct MapKey: Hashable {
let id: String
let values: [String: String]
}
in case you want to use a class, you need conform hash(:) func like this:
class MapKey: Hashable {
static func == (lhs: MapKey, rhs: MapKey) -> Bool {
return lhs.id == rhs.id && lhs.values == rhs.values
}
let id: String = ""
let values: [String: String] = [:]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(values)
}
}