Key Path Expressions as Functions

Wouldn't it be nice if:

  1. Enums had first-class support for key paths.
  2. Bool were simply enum Bool { case true, false }

That's all you'd need for \.view.isEnabled.isFalse to be derived automatically ;)

8 Likes

That is something I also want to answer the question regarding Optional, [nil, 42].count(where: \.isNone). However I'm not sure if we can swap the representation of a Bool from being a struct to an enum. If we could, then sure I'd support that change.

Wow, I didn't know about the force-unwrapping of keyPaths. That's pretty cool.

I just meant that those extensions may belong to user code, not to the standard library. The pitch is already good without this sugar.

To be clear, I wasn't saying that something like isFalse should be included into this proposal. Instead I think it's a reasonable thing to consider after this proposal was accepted, especially because operations with a predicate would miss the opposite state when used key-pathes as a simple machinery for reading values. Other than that, this proposal is just perfect.

I also do not like is isFalse. It leads to ambiguities like this:

struct Email {
  let isFalse: Bool
}
struct User {
  let email: Email
}
let user: User? = ...
user.flatMap(\.email.isFalse)

However, I think the proposal should be a welcome change. Anyone looking to be the implementer?

There is no ambiguity in your example.

Does this invert (User) -> Email or is it accessing isFalse? I chose a bad example here, but there could be cases in which isFalse could eater mean a property or the key-path-inversion syntax. I am for !. Sorry for the confusion.

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.