Key Path Expressions as Functions

This is already a "problem":

extension KeyPath where Value == Bool {
  var isFalse: (Root) -> Value {
    return { !$0[keyPath: self] }
  }
}

user.flatMap(\.email.isFalse)

The answer is that key paths do "maximal munch"; if you want to use members of the KeyPath type, you need more parentheses:

user.flatMap((\.email).isFalse)

(The example is extra contrived by the end, but hopefully you get my point: Swift already defines what this behavior should be.)

2 Likes

If I understand the pitch correctly it wouldn't work, because flatMap would take only functions and keypath literals, not keypaths.

Wouldn't it be better to define isFalse on Bool? Then you could use it like everyone wants, and it would be unambiguous. (I would rather call it toggled to mirror existing toggle)

I mean, you can do so if you want:

extension Bool {
  var isFalse: Bool { return !self }
}

but I think the general consensus is that isFalse would not be a good choice.

The name isFalse originates from the different thread mentioned above, where we also assume that Bool is kind of an enum with two cases called true and false:

Since I personally prefer writing boolean == false rather than !boolean, isFalse is automatically my preferred name for such property than toggled, but the name can be debated in another thread if we're truly going to re-evaluate the addition of a Bool property that returns the negated value.


My only point is that a Swift developer should not create his own extension for such a convenient as he discovers the key-path functionality added by this proposal and otherwise the lack of being able to invert the result returned from a function with a predicate.

We also should ask ourselves, what new algorithms can we express with a pitched extension, so in case of isFalse, previously it would have been only 'pure sugar' or third way expressing the same thing, but with this proposal it will actually allow inverting the result of functions with a predicate, a use case that has not existed by now.

I see

I was thinking along the lines of, "it already exists so we don't need a pitch to make something this simple easier", bit I guess it boils down to personal preference.

Not any that we couldn't if there was no isFalse.

foo.apply(\.bar.isFalse) vs foo.apply(!\.bar)

I don't understand that example.


Update: even the updated foo.apply(!\.bar) is not possible today. I don't see the value of extending ! operator to operate on key-pathes because it's not the key-path you negate.


With this proposal you can filter a list of all admins users.filter(\.isAdmin), but if you want to inverse the result of the function you currently would need to fallback to use the closure explicitly users.filter { $0.isAdmin == false }. I think this feels like a shortcoming which we could fix and make available for all Swift developers by default.

1 Like

I realize that. It was just stating my personal preference, but must have misread your comment. I was simply voting for the ! syntax.

prefix func !<Root>(rhs: KeyPath<Root, Bool>) -> (Root) -> Bool {
  return { !$0[keyPath: rhs] }
}
1 Like

As I mentioned above multiple times already, it's not the key-path that is negated, so it makes no sense from my perspective to create a new illusion here and overloading an operator. We should also avoid overloading operators as much as possible. That said there is nothing more that can be added to that discussion, so we should probably stop here. Any further discussion in that regard should be deferred until the current proposal went through the review.

4 Likes

Returning to the pitch itself, it is not clear to me why this feature should be restricted to keypath literals only.

I would hope and expect that a keypath stored in a variable or returned from a function could also be used in the same manner.

• • •

Additionally I am a strong proponent of Callable, and it would be great to make this work for any Callable type with the appropriate signature. I agree that key-paths should be callable.

7 Likes

Callable would indeed be useful.

Dang it, why do you always make so much sense :stuck_out_tongue:

I would think that callable would be implemented on key paths something like this:

extension KeyPath: Callable {
  func call(withArgument root: Root) -> Value { return root[keyPath: self] }
}

and when (if) functions some become Callable I'm sure a keypath could be implicitly converted to a closure.

I want to say some words against. In my mind, this feature needs more thoughts.

Compare theese 2 examples:

users.map { $0.email }
users.filter { $0.isAdmin }

vs

users.map(.email)
users.filter(.isAdmin)

The second variant is 4 symbols shorter. But, in my opinion, first variant is much more readable because of surrounding spaces and $0 argument.

The second variant is not intuitive and natural. For example, according to current swift grammar this expression can possibly be understood such way, that map closure returns an array of keypathes: Array<KeyPath<User, String>>. We must keep in mind some rules to understand it correctly. In fact, we see keypath in the code, but we must translate it into another mental construction inside our brain.
So, we are going to second paragraph.

It is said in proposal that this code is not allowed:
let kp = \User.email // KeyPath<User, String>
users.map(kp)

But this one is:
let f1: (User) -> String = \User.email
users.map(f1)

So, expression:
users.filter(.isAdmin)
means the following:
users.filter(.isAdmin as (User) -> Bool)

In fact, it is even more complex, because expression:
.isAdmin as (User) -> Bool
in fact means something like:
let closure: (User) -> Bool = { user in
return user[keyPath: .isAdmin]
}

Such rule is rather hard for understanding, especially for new users. We see a keypath (object), but should understand it as closure (function). It looks like some kind of compiler magic.

compactMap(.self) even 1 symbol longer than compactMap { $0 }.

We will see 3 constructions that make the same:
users.map({ $0.email })
users.map { $0.email }
users.map(.email)

First two are very similar semantically.
The first one is needed in if-let construction, because we can not omit parentheses:
if let user = users.map({ $0.email }) {
...
}

The second variant is used in other cases as syntactically more clear variant.

But after introducing the third variant, which one will be preferred by default?
In what cases one is more preferred than another?
Is it a good idea to combine them inside a scope?
Can we write adequate rule for linter to keep our code in tonus?

This proposal will add one more way to do well known things, but in more obvious way.
Every developer will have several variants of writing filter, map, etc. Different ways of writing the same things may confuse, will add noise to code and decrease its readability.

**Summary**:

In motivation paragraph of this proposal it is only said about more "shorter and sweeter syntax". But the price is:

  • Increase complexity of language rules.
  • Decreased readability, more noise / confusion. (it would be great to get comprehensible answers for questions from paragraph 4 in this message)

It seems that clarity/brevity balance will be worse after implementing this feature. Current syntax is compact enough, clear and unambiguous.

If there are no other arguable reasons except «shorter and sweeter syntax», this feature looks ambiguous, because the price is too high for such a little advantage.

I approved your post, but please be aware that this pitch was recently reviewed and accepted.

That is the reason I wrote the post. It the last chance for final thoughts.