Josh Homann
Josh Homann

Reputation: 16327

Can I make something like ExpressibleByKeyPathLiteral?

TLDR: Can I make something like ExpressibleByKeyPathLiteral to coerce the compiler to box my KeyPaths for me, or do these protocols use some kind of compiler magic to make them work?

The problem: Swift has ExpressibleBy*Literal family of protocols that let the compiler autoBox certain types like so:

struct Box: ExpressibleByIntegerLiteral {
    typealias IntegerLiteralType = Int
    let value: IntegerLiteralType
    init(integerLiteral value: IntegerLiteralType) {
        self.value = value
    }
}

func pbox(_ box: Box...) {
    for b in box {
        print(b)
    }
}

pbox(1,2,3)

I would like to know if there is anyway to do the equivalent for a Swift Keypath literal. For example I have:

struct PartialEquatableKeyPath<Root> {
    let keyPath: PartialKeyPath<Root>
    let yieldsEqualValue: (Root, Root) -> Bool
    init<Value: Equatable>(_ keyPath: KeyPath<Root, Value>) {
        self.keyPath = keyPath
        self.yieldsEqualValue = { $0[keyPath: keyPath] == $1[keyPath: keyPath] }
    }
}

extension Equatable {
    func isEqual(to other: Self, byComparing keyPaths:[PartialEquatableKeyPath<Self>]) -> Bool {
        return keyPaths.allSatisfy { $0.yieldsEqualValue(self, other) }
    }
}

Which I have to call like this:

let a = UIView()
let b = UIView()
b.isHidden = true
let keyPaths: [PartialEquatableKeyPath<UIView>] = [.init(\.isHidden), .init(\.frame)]
print(a.isEqual(to: b, byComparing: keyPaths))

What I actually want is something like this:

extension Equatable {
    func isEqual<SomeEquatable: Equatable>(_ other: Self, byComparing keyPaths:PartialEquatableKeyPath...) -> Bool {
        return keyPaths.allSatisfy { $0.yieldsEqualValue(self, other) }
    }
}

So I can just call it like this:

let a = UIView()
let b = UIView()
b.isHidden = true

print(a.isEqual(b, byComparing: \.frame.origin.x, \.isHidden))

Is there a way to do this? (I know I can get it to work with a generic, but only if all of the KeyPaths are of the same type, which is of very limited utility).

Upvotes: 1

Views: 159

Answers (1)

Zev Eisenberg
Zev Eisenberg

Reputation: 8158

I believe what you'd need here is Variadic Generics, which don't exist in Swift yet, but may exist some day.

As a workaround, you can make versions of your method that take one key path, two key paths, three key paths, etc., up to the highest number you think you'd reasonably need to use (and then maybe one more). You might be able to save yourself some time by generating the variants using Sourcery.

Upvotes: 1

Related Questions