Optional `hasValue`

Swift focuses much on readability and being a good first language for developers. Still, there are some rough edges where I think the language can improve.

For instance while Swift´s function syntax promotes very readable code, e.g. with named parameters and omitted parameter names for trailing closures, I think == nil and != nil checks disrupts the natural reading flow.

A small addition to the language could improve this - by adding a hasValue property to Optional.

For instance, instead of:

public var isAuthenticated: Bool {
   authToken != nil && refreshToken != nil
}

we could have:

public var isAuthenticated: Bool {
   authToken.hasValue && refreshToken.hasValue
}

It's also lets you choose if and guard depending on expression, for instance (in these examples, I'll comment out old code and add new hasValue-based code below):

public func resumeDownloads() {
    guard let item = downloads.first else { return }
    //guard item.resumeData != nil else { return } =>
    guard item.resumeData.hasValue else { return }
    performResume(with: item)
}
func downloadNext() {
    guard let next = downloads.first else { return }
    // if next.request != nil { return }
    if next.request.hasValue { return }
    performDownload(with: next)
}

Another benefit is that having this keyword removes the need for manually created, contextually convenient properties, which are common in many projects, e.g.:

class UserContext {

    var user: User?

    var hasUser: Bool { user != nil }
}

With hasValue, you can just use context.user.hasValue instead of context.hasUser.

The hasValue implementation in itself is trivial:

public extension Optional {
    
    var hasValue: Bool { self != nil }
}

This may have been proposed earlier, so excuse me of I've posted a duplicate.

2 Likes

Without wanting to sound excessively judgmental, my reaction: Oh, please, no!

This drove me crazy in C#, because I wanted to know what the difference between hasValue and a nil check was. If they were the same thing, why have 2 of them?

Evidence?

Evidence?

Evidence?

I absolutely don't want to discourage anyone from suggesting new things, but I do think the reason needs to be stronger.

12 Likes

Yeah, I can see that hasValue could conflict with some types, where hasValue may mean something else. I still think it's valid in the optional context, though. Plus, it'd be an addition to the language, not replace already existing functionality.

I still argue that == nil is not as naturally readable as the overall Swift syntax like if hasUser(user, respondedTo: comment), and instead more syntactical in nature, but I'll remove the unfounded makes your brain switch to programmer mode. The rest, you just have to take as my personal opinions, haha.

I have a long background in computer science (and yes, I've spent many years in C#) so I have no problems with these checks personally, but I still don't think they fit into the overall style of Swift. However, I was pretty upset about the removal of ++ and -- (but I saw why it was the right thing to do), so I can understand your reaction.

Thanks for the pitch.

My initial reaction is positive. This feels akin to the addition of Bool.toggle(). I especially like what this would do in the use case of ternary operators, which are quite common in SwiftUI code.

I think the following example reads substantially better:

// Current
Button(action: { … }) {
    Text(self.name != nil ? “Save” : “Cancel”)
}

// With pitch
Button(action: { … }) {
    Text(self.name.hasValue ? “Save” : “Cancel”)
}

Another analogue is collection.isEmpty. It’s roughly the same as collection.count == 0, but more readable. (I say roughly, because isEmpty also brings efficiency benefits, so isn’t primarily justified on readability grounds.)

7 Likes

I don't think the comparisons to Bool.toggle() and Collection.isEmpty quite hold here:

  • Bool.toggle() was proposed because it eliminates repetition that can be particularly verbose when the value being toggled is deeply nested in a dotted expression: some.long.member.access.expression = !some.long.member.access.expression.

    No such verbosity exists when checking == nil or != nil. (In fact, the proposed .hasValue is slightly more verbose, in terms of character count.)

  • Collection.isEmpty, as you pointed out already, can often be queried much more efficiently than computing the count.

Personally, I don't think x == nil or x != nil is as onerous or as much a readability problem as the original author claims it is. On the other hand, there is one place I could imagine this being more useful: enhancing optionals when using KeyPath-based APIs, because you would be able to write something like \.hasValue, whereas there's no equivalent of that using a nil comparison today.

If you want to push for something like this to be added, you might be better off focusing on something like that where the property would enable usage patterns that aren't currently possible in the language today (without writing your own extensions), rather than on subjective arguments about readability.

20 Likes

I'm cautiously +½ on this. I like that this would be more consistent with isEmpty (I might suggest isNil over hasValue though), and it also makes the comparison of Optional to a Collection-of-zero-or-one-values a bit more obvious.

I don't object to using == nil or != nil in principle. What I like about this proposal is that (IMO) it adds readability at the point-of-use. I think if value.isNil is more readable (and therefore approachable to new developers) than if value == nil.

4 Likes

This might increase readability for the affirmative case but possibly make it easier to miss negation, especially when used with a longer chain of properties.

if session.user.defaultAddress.country.hasValue { }

if !session.user.defaultAddress.country.hasValue { }

I also wonder whether isEmpty would be a suitable name for the property. With map and flatMap both collections and optionals have a concept of being containers, so there is a kind of conceptual consistency.

1 Like

Welcome to the Swift Evolution community! It's always wonderful to see people who are eager to help contribute to the process of improving the language.

I think the tenor of the feedback that's been given already goes to a basic point: Many of us have, I'm sure, thoughts about what existing features of Swift they would have designed differently if they were in Chris Lattner or the core team's shoes. But this isn't really the raison d'etre of Swift Evolution. Our aim here, if I may be so bold to speak in the plural, is to make changes (principally additive) to give Swift more capabilities: i.e., to enable the language to be used in ways that it cannot easily be used before. This is why @allevato writes:


It has been said that Swift is an "opinionated" language; one of the consequences of that is that any particular user may not share the same opinion as that presented by the designers of any language feature in question.

One of the features of Swift which really makes it possible for the language itself to be opinionated without hindering its users from being differently opinionated is the possibility of adding your own extensions--even to core types such as Optional, Int, and String. People have added custom operators, convenience properties, even retroactive conformances to protocols.

What this also allows us to do is to have a high bar for addition to the standard library. Essentially, the question you must answer is: what is it about your proposed change is valuable enough to justify not just your usage (which is already possible via extensions), but for everyone to need to learn it? (And make no mistake: even if people don't intend to use it, any addition to the standard library will need to be widely known because code is more often read than written.)

Some time ago, Ben Cohen wrote up a set of questions to break down this high bar into discrete questions to be answered when it comes to new API additions to the standard library, particularly when the implementation is a composition of existing features. I repost them here for convenience:


Now, as to the specific idea that you have proposed: you are correct that it has already been discussed in some form. (You can use the search function on this forum to see all the incarnations of it.)

But what's more, the same idea (including how it could enable certain usages not easily expressed otherwise, as suggested by some others here) was brought up in the context of the Result type (SE-0235). And not only was it pitched and proposed (with the names isSuccess and isFailure--but the specific names are rather unimportant here), it was reviewed by the community, eventually withdrawn from the final version of the proposal, and still nonetheless commented on by the core team in their final evaluation. So in fact your idea has actually gone through all the stages of Swift Evolution!

What the core team had to say was as follows (emphasis mine):

Case accessors are a more complex question. The current proposal doesn't include case accessors, but the original proposal did, and we're anticipating a future proposal that will add automatic synthesis for case accessors. That synthesis will undoubtedly use names based on the actual case names, so at a first glance, it seems imprudent to use case names that we're not sure we'd want for the accessors. However, feedback on this review has led the Core Team to more seriously consider this issue, and it now seems quite complicated. A name that's a good name for a particular state of an enum (like failure ) might not be a good name for a value carried in that case (like error ). It may be true that case accessor names will always end up feeling a little contrived, and so it's just better to use the actual case names because they're easy to interpret. More importantly, we don't want to burden this proposal because we haven't resolved those questions yet to our satisfaction.

In other words, the plan going forward which the core team is anticipating is that we will have a more general solution that applies to all enums, where if there is a case such as Optional.some, we will have some sort of accessor synthesized doing exactly what you pitch here, with a naming scheme to be decided.

14 Likes

Thanks for this background @xwu. It's very helpful to know what has been considered in the past.

1 Like

Thank you so much for your thorough input!

My goal with this tiny proposal was to increate readability and consistency with the overall language, but while I still think it's valid and can increase the correctness of the code people write, I absolutely understand that achieving this with your own extensions or public packages is probably better.

Seems like this has the potential to encourage worse code. It may make more folks reach for something like:

if foo.bar.baz.mumble.hasValue { 
  // do something with foo.bar.baz.mumble
}

If any of those properties are computed properties they would end up being computed multiple times. The better pattern is if let or guard let.

The initial downloadNext example in the pitch seems like a guard let _ = next.request else { return } might be a better pattern since it reuses the guard let that is going to be used in other places.

5 Likes

Yeah, those examples may not have been the best, and your're correct about the risk of recomputation, but in your example, you'd still need an if let before you can use the mumble, just like you would have if you'd have written the condition with an != nil check.

I disagree that such code would be more likely. Any Swift programmer that knows about if let is going to reach for it when appropriate. I don't see having .hasValue changing that.

I consider this an anti-pattern. Using guard/if let _ = ... to avoid checking against nil obscures the actual intent of the code. And if next.request is being invoked for a side-effect at that position, it's really poor form.

1 Like

Ah interesting. So something like myEnum.isSome and myEnum.isNone would be synthesized? That would also enable a workaround for one of the use cases for if case.

In practice the times I need to check if something has a value without also unwrapping it are pretty rare. and 90% of them are probably guard viewIfLoaded != nil { return }. I'm unsure if it's a meaningful improvement to do guard viewIfLoaded.isSome { return }.

I'm weary of adding too many small conveniences like this, especially to very foundational Types. If the situation is such that a significant amount of applications/libraries define them, (as was case with Result), it makes sense to add it to the language since everyone is doing it anyway. Otherwise, at least extensions make adding this kind of thing super easy!

How about just having implicit comparisons against nil?

Text(self.name ? “Save” : “Cancel”)

if str {
    // ...
}

I know it's intentionally disallowed, but is it really protecting against a class of bug? If the issue is checking an optional boolean, we could just disallow it for that scenario only.

if optionalBool { // Error: Optional boolean must explicitly check != nil
    // ...
}

I was thinking of opening a proposal, but I have seen this one open, so I present here the contribution.
I like this, IMHO is necessary that type of solution, but adding the counterpart.

public extension Optional {
    var isSome: Bool { self != nil } // other posibles names: hasValue |  isNotNil   or to open to discusion
    var isNil: Bool { self == nil }  // other posibles names:  isNone  | hasNotValue or to open to discusion
}

Current ways:

if let _ = sampleOptional {
    // isSome
} else {
    // isNil
}

if sampleOptional != nil {
    // isSome
}

if sampleOptional == nil {
    // isNil
}

Proposal way, I think is better to keep some syntax

if sampleOptional.isSome {
    // isSome
}

Dirty way (without var isNil: Bool)

if !sampleOptional.isSome {
    // isSome
}

More swift and readable:

if sampleOptional.isNil {
    // isNil
}
1 Like

Why not use isViewLoaded instead?

https://developer.apple.com/documentation/uikit/uiviewcontroller/2097563-isviewloaded

1 Like

:astonished: After 7 years of iOS dev somehow this is new to me...:joy: I guess that's what happens when you use CodeSense as your documentation most of the time. Thanks for making me aware!

2 Likes

This is exactly the direction I was hoping this thread would take :) I missed that the Core Team actually already considered it!
(Though personally I prefer having access to a separate property which is of the Discriminant/Case type, as it allows more use-cases)

1 Like