2

Here's the deal,

I'm writing an SDK, and I want to declare observers as protocols, instead of classes or structs (It's sort of an "Observer/Delegate" hybrid).

I want to be able to compare two arguments that are passed in as protocol references, as opposed to the concrete classes/structs they actually are, IRL.

I know that the "easy" way to get comparison is to constrain the protocols to Hashable or Equatable, but I want to avoid burdening the user (It's an SDK).

Here's a little playground with what I mean:

protocol A {
    func AFunc() -> String
}

class APrime: A {
    func AFunc() -> String { "I AM GROOT" }
}

let variableA = APrime()
let variableB = APrime()

func compareTypes(_ inA: A, _ inB: A) -> String {
//    if inA == inB {
//        return ""
//    }
    return "not "
}

print("A is \(compareTypes(variableA, variableB))B.")

print("A is \(compareTypes(variableA, variableA))A.")

The raunchy bit is the commented-out section in compareTypes(_: A, _: A). I need to figure out how to compare them without going into "Hacksylvania," which I could do by doing something like comparing addresses of the AFunc() in each instance.

The expected output is:

A is not B.
A is A.

Any ideas for a more "swifty" approach? I must be missing the forest for the trees.

Chris Marshall
  • 4,910
  • 8
  • 47
  • 72
  • 2
    Suppose ClassA and ClassB conform to protocol A, and you have instances `a` and `b` of those classes respectively. What could it possibly mean to compare them for equality _qua_ protocol A? You can't even get started unless you can guarantee that `a` and `b` are the same type. — Note that this is exactly the kind of problem that Swift 5.1 `some A` solves. If you don't want to go that route, you're going to need to engage in some sort of type erasure, so that what you are comparing are A-wrapper class instances. – matt Feb 16 '20 at 19:58
  • Yeah. That's what I'm looking at. "some" is nice, but I'm not sure that it solves the issue. I'm looking for _identity_ equality, not _type_ equality. I have a feeling that I'll end up having to force the protocols to be Equatable, but I wanted to avoid that. – Chris Marshall Feb 16 '20 at 20:00
  • 1
    I don't think you are quite accepting _what_ is equatable. What I'm saying is that the entire notion of equatablity _qua_ protocol is incoherent. A protocol is not a thing. An _instance of a protocol adopter type_ is a thing. The whole wording of your title, "protocol instance", seems to betray a failure to grasp that point. There is no such thing as a "protocol instance". – matt Feb 16 '20 at 20:01
  • Another possibility is to force the implementor to provide a UUID variable, which gets a unique UUID assigned by the SDK. I have done that before. It's kludgy, but works. – Chris Marshall Feb 16 '20 at 20:01
  • 1
    "I'm looking for identity equality, not type equality." That's what `===` is, and it's only applicable for classes. Structus don't have identity, because they're constantly moving around in memory. Copies of structs causes copies of values. Copies of classes cause copies of their references, but both references still point to the same unique (as far is "identity" and memory location goes) object in the heap. – Alexander Feb 16 '20 at 20:13
  • Nah. I get it, but you are correct to point out my incorrect use of terminology. Protocols are weird, if you are trying to be concrete. I suspect that I'll probably go with the UUID thing. I already know it works, and there's prior art, as Apple does it with the window property in the UIApplicationDelegate class. – Chris Marshall Feb 16 '20 at 20:13
  • @Alexander - Reinstate Monica (Love the tag, BTW), you are correct, but. as Matt pointed out, I'm really trying to fit a square peg into a round hole. I was just hoping that I'd have some "easy out" that I missed. – Chris Marshall Feb 16 '20 at 20:15
  • @ChrisMarshall You should elaborate for what the actual desired semantics are here. There are different plausible notions of equality, each make sense in same cases, but don't in others. Equality by value makes sense for `BigInt` (your `BigInt(123)` should be equal to my `BigInt(123)`, even if we're talking about to two distinct objects), but not for `Person`. (your `Person("Jane Smith") might model a different person than my `Person("Jane Smith")`). There's no universally correct answer, and no "sensible default". – Alexander Feb 16 '20 at 20:22
  • @ChrisMarshall Hence why `Equatable` exists, so that you can define for yourself what the correct notion of equality is for your types. As a library author, unless you're working in the context of a particularly well defined domain, there isn't a sensible default that you can resort to. Requiring `Equatable` isn't a burden, it's necessarily descriptive. – Alexander Feb 16 '20 at 20:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/207944/discussion-between-alexander-reinstate-monica-and-chris-marshall). – Alexander Feb 16 '20 at 20:24

1 Answers1

1

Just to add some closure to this, here is how I solve this:

protocol A {
    var uuid: Int { get } // This is the secret sauce. It will contain a unique UUID, associated with the instance.
    func AFunc() -> String
}

class APrime: A {
    let uuid: Int = Int.random(in: 0..<1000) // The UUID is initialized with the instance.
    func AFunc() -> String { "I AM GROOT" }
}

let variableA = APrime()
let variableB = APrime()
let variableC = variableA

func compareTypes(_ inA: A, _ inB: A) -> String {
    if inA.uuid == inB.uuid { // We compare UUIDs.
        return ""
    }
    return "not "
}

print("C is \(compareTypes(variableC, variableB))B.")

print("C is \(compareTypes(variableC, variableA))A.")

The "uuid" variable is usually an actual UUID type, but I didn't want to import Foundation in the example, so I just did a simple rand. It gets the point across.

This outputs:

C is not B.
C is A.

And there is another way (that I also use, sometimes):

protocol B {
    func BFunc() -> String
    func amIThisOne(_ instanceToCompare: B) -> Bool // This is an identity comparator
}

class BPrime: B {
    func BFunc() -> String { "I AM GROOT'S BROTHER" }
    // We compare ourselves against the other instance, assuming it can be cast to our own type.
    func amIThisOne(_ inInstanceToCompare: B) -> Bool {
        guard let instanceToCompare = inInstanceToCompare as? Self else { return false }
        return self === instanceToCompare
    }
}

let variableD = BPrime()
let variableE = BPrime()
let variableF = variableD

print("D is \(variableE.amIThisOne(variableD) ? "" : "not ")E.")

print("D is \(variableD.amIThisOne(variableF) ? "" : "not ")F.")

Which outputs:

D is not E.
D is F.

This allows a more programmatic way of comparing the instances.

HOW NOT TO DO IT

And then, of course, if we have control of the instances, we can truly do the Equatable thing (This requires that the playground import Foundation):

protocol C: Equatable {
    func CFunc() -> String
}

class CPrime: C {
    // This is actually not what I want, as I want to compare protocols, not conforming classes.
    static func == (lhs: CPrime, rhs: CPrime) -> Bool {
        guard let lhs = lhs as? Self else { return false }
        guard let rhs = rhs as? Self else { return false }

        return lhs === rhs
    }

    func CFunc() -> String { "I AM GROOT'S UDDER BROTHER" }
}

let variableG = CPrime()
let variableH = CPrime()
let variableI = variableG

print("G is \(variableG == variableH ? "" : "not ")H.")

print("G is \(variableI == variableG ? "" : "not ")I.")

Which outputs:

G is not H.
G is I.
Chris Marshall
  • 4,910
  • 8
  • 47
  • 72