Negate KeyPath Values with not()

Swift KeyPath is a great shortcut for property-based mapping and filtering of collections:

bitcoins.map(\.dollarValue)

When you need a simple transformation, the simplest probably being the boolean negation operator, you only have two choices:

toggles.map(\.isActive).map { !$0 }
toggles.map { !$0.isActive }

So either you use the key path and separate the negation – which makes it harder to read as “I want the opposite of X”. Or you drop the key path and use a regular closure.

I was fed up with this and introduced a not operator:

func not<T>(_ keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
    return { value in
        !value[keyPath: keyPath]
    }
}

Then you can write:

toggles.map(not(\.isActive))

There’s no way to create new KeyPath objects from scratch unlike, say SwiftUI.Bindings. So there’s no cute way to express this operator as a function like inverted() and get \.isActive.inverted().

I have a hard time coming up with examples for integers. Any I do recall are from tutorials about partial function application and currying. But nobody needs an add2 function in the real world. The idea is the same, on a level so abstract it’s almost useless to point out: use free functions and combine them.

Update 2024-03-30: Soroush Khanlou pointed out that the following extension on Bool should work:

extension Bool { var inverted: Bool { !self } }
toggles.map(\.isActive.inverted)

And indeed it does! In hindsights, it’s also obvious that this compiles, because you can create key paths deeper down an object and it’s associated properties with this short-form.

Here’s what I have tried that didn’t compile:

let keyPath = \Toggle.isActive
keyPath.inverted // 🛑 error: value of type 'WritableKeyPath<Toggle, Bool>' has no member 'inverted'

That makes perfect sense, because the key path doesn’t have a member called inverted, and that was my point above where I mentioned that you can’t write a nice extension on key paths for the job.

Meanwhile, keyPath.appending(path: \.inverted) would have worked, and the backslash-period shorthand is just syntactic sugar for this. The literal “.inverted” isn’t applied to the key path there if you stick to that one expression.

Thanks, Soroush!