[Pitch] Add KeyPaths Boolean custom operators

SE-0249 introduced the ability to use the key path expression \Root.value wherever functions of (Root) -> Value are allowed.

This is pretty awesome and now well used across projects.
What I'm trying to propose here is to add some new operators for key paths to make it easier to use it when trying to apply any logic to it.

Here is a couple of examples:

Let's say we would like to filter a dictionary and get all values that are true.
We can do this pretty easily with key path now by doing:

let dict = ["a": true, "b": true, "c": false]
let filtered = dict.filter(\.value) // returns ["a": true, "b": true]

Pretty cool but what if we want to filter the dictionary for false values?
Well now we can't use key path and we need to rely to the closure:

let filtered = dict.filter { !$0.value } // returns ["c": false]

The proposal here is to add the following operators:

prefix func !<V>(keyPath: KeyPath<V, Bool>) -> (V) -> Bool {
    { !$0[keyPath: keyPath]}
}

func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] == rhs }
}

func !=<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] != rhs }
}

This would give us the ability to use key path as following:

let dict = ["a": true, "b": true, "c": false]

let filtered = dict.filter(!\.value)

let filtered2 = dict.filter(\.value == false)

Would appreciate any input here!

8 Likes

In my opinion, you do not need an extra bit of syntactic sugar if you can easily introduce it in your own code, like in this case. Then everyone will be free to decide if their code base can benefit from it.

You can also do what you want with:

extension Bool {
    var toggled: Bool { !self }
}

let filtered = dict.filter(\.value.toggled)
9 Likes

I agree. Also, overloading the !, ==, != operators is not such a generic solution. If we accept that keypaths are really similar to functions, any negation/toggle operations should happen through a generic map function or pipe operator. This way, the keypath's result is piped to the ! function which again returns a boolean.

1 Like

While what you're talking about should be part of the standard library, negation is too common to not be in the standard library too. Just like toggle(), the lack of it won't last. Everybody needs it, all the time.

(toggled, as shown above, is not an accurate name for this, however—"toggled" means you flip the thing via mutation first and then return the flipped value. That doesn't work with key paths.)

public extension Bool {
  /// Toggle, and return the new value.
  var toggled: Self {
    mutating get {
      toggle()
      return self
    }
  }
}

I don't think the other ones add anything.

(\.value == false)
{ $0.value == false }

The general form works for key path expressions:

infix operator …

public func … <Root, Value, Transformed>(
  value: @escaping (Root) -> Value,
  transform: @escaping (Value) -> Transformed
) -> (Root) -> Transformed {
  { transform(value($0)) }
}

public prefix func !<Root>(
  getBool: @escaping (Root) -> Bool
) -> (Root) -> Bool {
  getBool…(!)
}

But you also need the specialized form to deal with key paths when they're variables:

public extension KeyPath {
  /// - Returns: A closure that returns a transformed `Value`.
  func map<Transformed>(
    _ transform: @escaping (Value) -> Transformed
  ) -> (Root) -> Transformed {
    { transform($0[keyPath: self]) }
  }
}

public extension KeyPath where Value == Bool {
  static prefix func !(keypath: KeyPath) -> (Root) -> Value {
    keypath.map(!)
  }
}
1 Like

I think you have this backwards - the standard library is pretty consistent that the past tense (toggled, reversed, shuffled, lowercased, rounded) means the original value is left unaltered, where the imperative (toggle, reverse, shuffle, round) means the original value is mutated in place. Judging by that pattern, toggled is a perfect name for that computed property.

4 Likes

I could also think of these past participles as being phrased in the present perfect tense. But I think we're just supposed to see them as participial adjectives. The guidelines could stand to be more educational.

My issue with "toggled" is that I haven't been able to imagine "toggling" something not directly triggering side effects. I.e. that which is toggled is not really the Bool itself, but whatever relies on that Bool as a model. Thanks for causing me to reconsider.

The documentation is really not helping things… :face_with_diagonal_mouth:

You can use any name you like: toggled, negated, or inverted :blush: