Edward Anthony
Edward Anthony

Reputation: 3464

Is it possible to make a Heterogeneous Set in Swift?

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?

Upvotes: 0

Views: 852

Answers (2)

Edward Anthony
Edward Anthony

Reputation: 3464

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:

without self and with self comparison

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))

Upvotes: 5

Ahmad F
Ahmad F

Reputation: 31645

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.

Upvotes: 0

Related Questions