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

Implementation:

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

Usage:

let trueBool = true
print(trueBool.toggled) // false

Rationale:
toggled provides ease-of-use in functional contexts.

E.g.
In the status quo, one way to filter whole numbers from a string would be as follows.

let string = "abc12345" 
let nonDigits = string.filter { !$0.isWholeNumber } // "abc"

However, such an approach does not leverage the benefit key paths introduced in Swift 5.
You can see these in use in the following expressions.

let string = "abc12345" 
let nonDigits = string.filter(\.isWholeNumber.toggled) // "abc"

Besides brevity, a major advantage is that when used in an if-let or guard-let clause, the filter expression will not need to be wrapped in both parentheses as well as curly braces.

Prior art:
Other Swift types offer similar interfaces that can be drawn on for inspiration. Take the Sequence for instance.
It offers two methods, one which mutates, reverse() and another reversed() which does not.

var collection = ["A", "B", "C"]
print(collection.reverse())
// ["C", "B", "A"]
print(collection)
// ["C", "B", "A"]
let collection = ["A", "B", "C"]
let reversedCollection = collection.reversed()
print(collection)
// ["A", "B", "C"]
print(Array(reversedCollection))
// ["C", "B", "A"]

The prior art would imply a corresponding set of methods on the Bool type.
.toggle() already exists on Bool.

var bool = true
bool.toggle()
print(bool)
// false

That makes it plain the obvious omission of.toggled on Bool, that would clean up a lot of filtering code.

This approach provides greater utility to developers to the swift ecosystem, who would either

  • declare toggled/toggled() in each code base worked on
  • use curly braces simply in order to access the '!" not operator.
  • as mentioned at the outset of my post, not having to wrap curly braces in parentheses in the context of if-let and guard-let statements where the only reason to do so would be to access with "!" not operator.
    The resulting benefits include:
  • code deduplication through declaring such a property in Swift standard library

Previous thread: [Proposal] Add property for negation to Bool - Evolution / Discussion - Swift Forums

4 Likes

I like it because it's not as tempting to "misuse" this for ordinary if conditions as if it was named e.g. "not". And it nicely complements toggle() :+1:

I'm just fearing stuff like

if isEven(x).toggled {
  doStuff(x)
}

Perhaps we could have a fixit suggesting isEven(x) == false instead? :slight_smile:

2 Likes

There is no operator that performs the equivalent operation to the reversed method on Sequence, whereas there is one that provides the equivalent operation on Bool.toggled. Because this operation is already provided by an operator, the extra property is redundant.

I’m not sure anything has changed in the last five years to warrant repeating the conversation again. It’s unlikely to surface any new arguments which haven’t already been discussed at length.

I’ll reiterate my point from before that Swift doesn’t vend alternative spellings for the same API. While prefix operators are more cumbersome to use in certain idioms that involve lots of chaining, so are free functions; if that’s deemed a significant enough usability problem then an appropriate solution is some feature that can be used to solve this problem for all such cases, not a piecemeal duplication of one API at a time.

But to circle back, starting up a new thread doesn’t seem like a very productive use of the community’s time, as nothing fundamentally has changed since the numerous previous discussions. If it has, it’d be worth focusing on that here. Otherwise, everyone can browse the many previous threads at their leisure.

6 Likes

As I understand it, the main selling point here is that having a property facilitates accessing the value via a keypath, which is more convenient in some cases.

1 Like

It would only be more convenient for APIs that require a keypath, as opposed to those that accept a closure but which can satisfied by a keypath literal. For this reason, the string.filter(\.isWholeNumber.toggled) example is a bad one. string.filter { !$0.isWholeNumber } is actually more brief.

2 Likes

This properly was also often referred as isFalse in the forum.

This is not a free function though. I for one agree that there should be a way to use negation in keypath expressions, and ideally in chaining.

Presume for a moment that string is optional.

if let x = string?.filter(\.isWholeNumber.toggled) { print($0) }
is clearer than (and nearly a brief as)
if let x = string?.filter({ !$0.isWholeNumber }) { print($0) }
and it is more more brief than not using anon. variables for the filter clause,

if let x = string?.filter({ number in,
  number.isWholeNumber }) { 
 print(x) }
}

In short, that this syntax like toggled or isFalse may introduce an extract character is made up for in terms of readability. One or two characters does not seem a good reason to omit an API that can improve keypath chaining, a first class feature since Swift 5 that requires the utmost support for it's mamy widespread use cases. In other words, the proposal for toggled or isFalse is decidedly not an issue about "could golf' see how much few character can be used to execute the same correct code.

Further, there are certainly scenarios where a "!" cannot apply. Let's say you map one object to another using keypaths, for instance. There is no place to put a not operator.

You could put it in front of the keypath, you can probably make a function that transforms keypaths, at least if you create your own private .toggled.

This is what I am suggesting: a .toggled or isFalse property on Bool. There is precedent with reversed() and reverse() methods already available in Swift.

Of course, everyone can write their own .toggled property. That would only negate the purpose of having a standard library.

And I'm suggesting an operator on KeyPaths. I'm just saying it might need to be powered by a (private) .isFalse property if you want to make something like that yourself.

1 Like

But .toggled() cannot be used with key paths, since key paths don't support functions. I think adding toggled() doesn't solve the problem you mentioned.

1 Like

+1. "toggled" matching "toggle()" is what i like. but as i said in another thread that ship has sailed, so just define it in your own codebase and treat it as if it was part of the standard library.

i remember there was a litmus test for proposals' inclusion along the lines that "if we designed swift today would we do it or not". would we use the same symbol "!" for boolean negation and optional unwrapping? i'd say not. do we still follow this litmus test guidline and does it mean we need to deprecate ! for boolean negation? i'm afraid not and no; it's so deep in swift's DNA.

It seems to me that the property name toggled has no semantic meaning. To toggle something is to change the state from current to new. To turn off a switch that's on. To change a boolean from false to true. Without knowing the current state, asking if it's toggled is a meaningless question. By definition, it's not toggled, because toggling requires a change in state and querying the current state is not changing it.

As to the general idea, I see a value in having isFalse. Operators are not always convenient to use.

5 Likes

it's fine to me - like getting an "amended bill" in mail or like "returning a clone of a switch with the value toggled compared to the original". i do this all over the place (e.g. in my extensions to CoreGraphics i have "translate()/translated()/translation()", "invert()/inverted", and so on).

"isFalse" looks strange to me but better than nothing. i also wouldn't mind "not" or another name.

I would suggest that the continued growth of KeyPath APIs over the last 5 years indicates a direction the rest of the language might want to enable. Whether it justifies this API I don't know, but there is at least one new consideration since toggle() was added.

5 Likes

I certainly agree with your rationale against a piecemeal approach to what you call "alternative spellings" for existing APIs.

However, the discussion I propose it not to replace "!" with "not" and "&&" with "and" as was the discussion of the previously discussion you refer to.

This discussion pertains to the use of existing operators in conjuction withe keypaths, in order to simplify functional programming statements such that curly braces and the boilerplate of anonymous parameter names, can be safely omitted. As I have mentioned, these will improve readability and writability.

  1. anonymous closure arguments can be cumbersome to read to non-authors
  2. in the context of if-let and guard-let statements, curly braces do not suffice and wrapping curly braces in parentheses may be error prone when writing--after all, they can be quite difficult to discussion between even when using a reasonable editor text size.

If you have alternatives to propose that may not only apply to negating boolean properties, all interested parties here will be very interested to hear them. If not, at the very least, isFalse or toggled may prove a jumping off point for further such discussions.

Thank you @xwu for your interest in the topic at discussion.

I struggled with finding a way to express wanting a logical inversion of a filter based on a KeyPath to a Bool when I recently learned how KeyPaths work and gave up on using a KeyPath in that case, so I can empathize with the motivation here. However, perhaps the right thing is to find a way to express an inversion of a KeyPath to a Bool instead of unnecessarily expanding Bool's API.

However, perhaps the right thing is to find a way to express an inversion of a KeyPath to a Bool instead of unnecessarily expanding Bool 's API.

I agree with your sentiment, however, such a solution escapes me. It looks like the key path code only deals with downcats.
swift/KeyPath.swift at main · apple/swift (github.com)](https://github.com/apple/swift/blob/main/stdlib/public/core/KeyPath.swift))

So, something like this may not work,

extension KeyPath where Value == Bool {
  var not: KeyPath<Root, Bool> {
    return <#...#>
  }
}