`toggled` or `isFalse` property/method on bool for use with KeyPath APIs

This is exactly what I expected to already work when I was trying to filter based on a KeyPath to a Bool.

I think that a Bool.toggled property would be too similar to the existing ! operator and could cause confusion with new users about which they should use.

Also, the primary motivation of key path expressions being usable as function types is to be able to refer to instance properties in a slightly more convenient way. Key path expressions are not meant to be a replacement for closure expressions. Additionally, I don’t think
string.filter(\.isWholeNumber.toggled)
offers any benefits in terms of clarity or conciseness over
string.filter { !$0.isWholeNumber }
to most Swift programmers — I doubt there are many who know about key paths but don’t know about the ! operator. Even if you find the shorthand $0 argument name confusing, you could name the argument something like character:
string.filter { character in !character.isWholeNumber }

I don’t think we should add an ! operator that’s only for key paths where the value type is Bool like @GreatApe is proposing. However, I am interested in adding support for operators in key path expressions and key path–based @dynamicMemberLookup types. There have been previous discussions on these forums about allowing instance methods to be referenced within key paths:

and I think that allowing unary operators in key paths like
string.filter(!\.isWholeNumber)
is something that should be considered as well. Core Team member @Joe_Groff has stated that there’s no fundamental reason key paths can’t support instance methods, and I assume the same thing is true for operators since their implementations are similar. This syntax is intuitive, too:

Hm, so in the end you support operators on keypaths?

1 Like

I think it’s worth looking into.

I don’t think it should be done by extending the Bool type and creating a new ! operator that operates on the KeyPath<_, Bool> type like you’re proposing, however: instead, I’m proposing that the implementation for key paths be changed so that it supports unary operators and instance methods.

I’d like to see what other people think of my proposal, though.

1 Like

Aha. Yeah that would be nice. The KeyPath would forward all operations to the value it points to. Sort of like the way dynamicMemberLookup works for Bindings but when applying functions.

Negation is definitely my most common use case for that, but when the key
path points to an array it would be nice to be able to bake in a .filter or .map into the keypath itself.

I think currently KeyPath.map() is not possible because of equality. It is similar to indexing key paths which capture the index and require index to be Equatable (maybe even Hashable). Similar here - this would require Equality of functions.

Why does the function you map have to be equatable? How is it different from mapping an array?

Ah, sorry, I misread the code as returning KeyPath<Root, Transformed>. If it’s just a (Root) -> Transformed, then equality is indeed not a requirement.

1 Like

Ah, you mean if the function is baked into the keypath, and we want keypaths to be equatable then functions have to be as well. Sure that's true. But I would settle for something like MappedKeyPath which is not equatable but otherwise has the nice properties of KeyPath.

I agree with your points, especially about allowing unary operators in key paths, which is a good solution unless something else like Jessy's suggestion for map gets used, which is a brilliant path forward in it's own write.

What I'd like to highlight about the point you make about one syntax, the one I proposed, not offering much benefit over the existing one is that small syntactic differences add up in complex codebases. I don't presume that you lack any experience working in lengthy codebases that make extensive use of key paths. For that reasons you must understand the benefits of not using anonymous arguments where they are not necessary (as they introduce boilerplate, even if it's of the most innocuous kind), and not using curly braces where they are not necessary to use, especially as code bases are nearly always shared between programmers of various experience levels.

Using curly braces as a substitute for key paths where there is no justifiable reason for not using them considering the alternatives suggested in this thread impairs readability in particular because curly braces are used in if/guard/if-else as I mentioned at the outset. With key paths, let's say e.g. if you use a contains statement, as a writer you won't need to use both parentheses and curly braces in order to place your contains test in an if statement.

In short, fewer anonymous arguments and fewer curly braces wrapped in parentheses for the sake of can only help to improve readability, not to mention brevity, which would be a secondary concern over readability in most cases.

I really appreciate a proposal that would not extend the Bool type to create a redundant property. If there's a way to leverage the existing ! operator, that would be absolutely fantastic.

This already compiles. (Based on my previous code above.)

XCTAssertEqual(
  [true, true].map(!\.self),
  [false, false]
)
public extension KeyPath where Value == Bool {
  static prefix func !(keypath: KeyPath) -> (Root) -> Value {
    keypath.map(!)
  }
}

If the language adds the feature for extending closures, so more operators can be chained, is that enough?

Personally, I don't understand what a KeyPath is, and I'm not sure anybody does. The structured documentation doesn't define what "key" or "path" are. Does it fundamentally have to look like something that Mirror can work with, or does it just exist because { $0… } is ugly? 🤷

1 Like

Sure, keypaths are definitely a mystery. But practically speaking, it's nice to be able to create something type safe using a clear and succinct syntax, with syntax completion, store it if you want, and apply it to an instance.

I don't care about Mirror, but it would be great if you could

  1. Refer to the existing instance methods of a leaf property, not just it's properties
  2. Apply external functions to the leaf property

and still have a KeyPath, or at least a subtype of that.

Key paths are references to properties. They are usually not useful, but extremely useful when they are. They may be used to produce extremely powerful APIs, and allow consumers a level of flexibility normally associated with runtime manipulation.

While I do not actually know for sure, I consider it probable that compile-time optimization is easier for more constrained parameters like key paths than for closures, which have to have their own scope and everything. It is certainly easier to reason about when reading code.

Personally, I think every declaration in Swift should be accessible via key path, unless there is a compelling reason they should not be. Operators are glorified functions, and they should be treated as such when it comes to key paths. Same with subscript methods, which would be really nice for drilling down into dictionaries via key path.

A modest alternate proposal: properties of type bool or functions returning bool automatically get a negation generated whenever Swift can infer a suitable name.

So var isXxx: bool implicitly also defines a var isNotXxx: bool with the obvious implementation, such as isNotWholeNumber or isNotHidden.

And same for func isXxx(...) will implicitly also define func isNotXxx(...), such as isNotDetailLink(...).

There could be a small set of these well-defined inference rules, "doesXxx" infers "doesNotXxx", "shouldXxx" infers "shouldNotXxx", "canXxx" infers "cannotXxx", "willXxx" infers "wontXxx".

But these rules doesn't work ideally in many cases: the above example isNotHidden could arguably instead be isShown, isNotEven would clearly be better named isOdd. And of course there are many bool functions and properties are not spelled like "isXxx" at all, like contains(...) whose negation should maybe be omits(...). For these an attribute @negation(name: isOdd) can correct an inferred negation or provide one when none can be inferred. And for the few cases where a property or function shouldn't get any negation when the rules normally would infer one, then they can be annotated with @negation(none).

I'm maybe 3/4's kidding.

But maybe even without dubious language support, we could foster the practice of defining negation properties and functions manually. Who knows one day they could be regarded as "Swifty". Or could possibly a @negation attribute on its own be a good idea after all without the flaky inference part?

2 Likes

Maybe (surely?) this has come up in previous discussion, but the obvious solution having spent time with Ruby is to throw a reject function into your project, which is the opposite of filter. I understand that kind of thing is never going to make it into the standard library though.

4 Likes

We'll never know until it's proposed. I give it a strong +1.

1 Like

@Saklad5 you very precisely explicate the core of the issue here (emphasis mine). Your language needs to go into a proposal. I personally applaud your perspective to not only mention the benefits that adding support for operators in key paths would grant by expanding the discussion to the potential benefits of key path support for subscript methods.

What are subscript methods? I think key paths for subscripts are already supported as ["Swift": 5.5][keyPath: \.["Swift"]] works.

Yes, key path subscripts works as expected.

I believe the author @danhalliday was referring to these when mentioning subscript methods.

extension Collection where Indices.Iterator.Element == Index {

  subscript (safe index: Index) -> Iterator.Element? {
      return indices.contains(index) ? self[index] : nil
  }
}

let x = [1, 2, 3]

print(x[keyPath: <#...#>])

I always forget that replies don't appear as replies when they are first posted, thus the deleted previous comment. Someone link me to the discussion forum JIRA I'll file a ticket. :smiley: