Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2016-02-29

Comparing two protocol implementers for identity

An example on how to compare two items that implement the same protocol for their identity. I.e. how to know if two protocol implementers refer to the same item?

I was recently implementing a callback protocol and I needed to be able to manage a list of targets for the callback operations. The basic idea was this:

protocol MyProtocol {
    func myCallback()
}

var callbackTargets: Array<MyProtocol> = []

func add(p: MyProtocol) {
    callbackTargets.append(p)
}

func remove(p: MyProtocol) {
    for (index, q) in callbackTargets.enumerate() {
        if q == p { callbackTargets.removeAtIndex(index) }
    }

}

However that does not compile, it results in the following error:

error: binary operator '==' cannot be applied to two 'MyProtocol' operands

Hmm, ok lets try the '===' operator...

func remove(p: MyProtocol) {
    for (index, q) in callbackTargets.enumerate() {
        if q === p { callbackTargets.removeAtIndex(index) }
    }

}

No luck, the same error:

error: binary operator '===' cannot be applied to two 'MyProtocol' operands

Seems we need to put on our thinking caps...

What is happening?

For the first error, it is probably caused by the fact that no equality operator is defined for the protocol. Since this would impose on the the item that implements the protocol I do not want to add an equality operator. Besides, it would be necessary to compare different kinds of items with each other. Which opens up a whole new can of worms (I think it can be done, but I have not investigated that).

The second error probably comes from the fact that a protocol can be implemented by a value type as well as a class type. A struct can implement a protocol, as can a class. Since it is not possible to compare value types to objects the compiler justifiably complains about it.

There is an additional problem under the hood about which the compiler does not complain, but which is likely to lead to unintended consequences: The array must behave as if it contains "Any" kind of items. Which means that for value types there will be an implicit copy in the "add" function. One of the results will be that when we call the "myCallback" function on the items in the array it won't be on the original item but on a copy of it. Which in my case would be the wrong behaviour.

So, how to solve this?

The first -and most obvious- solution would be to introduce a constraint on the protocol. Implementing a callback really only makes sense for objects. It is unlikely that a value type would need the callback. We could therefore constrain the protocol to class types only:

protocol MyProtocol: class {
    func myCallback()
}

var callbackTargets: Array<MyProtocol> = []

func add(p: MyProtocol) {
    callbackTargets.append(p)
}

func remove(p: MyProtocol) {
    for (index, q) in callbackTargets.enumerate() {
        if q === p { callbackTargets.removeAtIndex(index) }
    }

}

This works. But what if we don't want to add the constraint?

Well, then we need to ensure that the items in the array are all of the same type. For example by making it explicit that we want to deal with pointers:

protocol MyProtocol {
    func myCallback()
}

typealias MyProtocolPointer = UnsafePointer<MyProtocol>

var callbackTargets: Array<MyProtocolPointer> = []

func add(p: MyProtocolPointer) {
    callbackTargets.append(p)
}

func remove(p: MyProtocolPointer) {
    for (index, q) in callbackTargets.enumerate() {
        if q == p { callbackTargets.removeAtIndex(index) }
    }
}

Two possible solutions, I have no preference for either, but in my case it is very unlikely that the callback will ever be implemented on a value type, hence I will constrain the protocol.

PS: Note that in a real application you would want to check if a callback target is  already present in the array with callback targets before appending it.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment