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.Binding
s. 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!