The Optional Truth

Why does optional chaining in the expression in an if statement require explicit comparison to true?

Consider the following code where optional chaining is used in if statements:

struct One {
    var isImportant: Bool = true

    func doSomethingImportant() {
    }
}

func test(one: One?) {
    if one?.isImportant { // Error: Value of optional type 'Bool?' not unwrapped...
    }
    if one?.isImportant == true { // no error
    }

    one?.doSomethingImportant() // no error
}

The error states that the Bool? has not been unwrapped. That's what optional chaining does. It safely unwraps.

I understand that == true doesn't actually unwrap an optional? It creates a boolean expression by the direct comparison.

Why doesn't the compiler insert an implicit == true to if statements when the expression uses optional chaining? It seems that the compiler always inserts == true to if statements that evaluate to Bool.

Is there a place on this site that explains how to format posts?

2 Likes

Hi @phoneyDev, would you mind fixing the markdown formatting of your post for better readability?

Single back-ticks for:

`keywords or single lines`

or tripple back-ticks for:

```swift
for code blocks
of multiple lines
```

I guess true is automatically converted to Bool?, and the equality check is implemented for this type as well — so it is no compiler error.
one?.isImportant can be nil, so it can't be used as condition.

if one?.isImportant { // Error: Value of optional type 'Bool?' not unwrapped...

one?isImportant may evaluate to nil. Nil is not a Bool.

Actually no, optional chaining does not 'just' unwraps the value but allows you to chain expressions that are optionals further. In that sense, since One is wrapped into an Optional you do chain the value in one further and try to access a member of the wrapped type, which then will become also an Optional.

  • If one is nil then isImportant will also be nil
  • If one is not nil the compiler cannot statically know if isImportant is still nil or not and you have to unwrap it to check
  • one?.isImportant == true works because it uses an overloaded == function for optionals which tries to unwrap both values and then compare those.

Sure, here you go. (Not on this site though.)

@Marco_S_Hyman Yes. And nil isn't == true. If the compiler did what I asked (implicitly insert == true for optionals or optional chains) then nil would evaluate to false, as expected.

@DevAndArtist Your third bullet item is the answer. This is an implementation detail. There is a library function that does the comparison of optionals and returns a Bool and it's hidden behind operator overloading. For whatever reason the compiler doesn't do what the library function does do so we're left with this odd (IMO) code requirement of comparing optionals to true. This could have been implemented in the compiler but I assume someone decided that the current way is better.

I'll say that coming from Objective-C and other C-style languages using == true in if statements is beginner code. Seeing it in swift makes me cringe a little every time.

Maybe you know what the design rationale behind this implementation is?

1 Like

Suppose you have a value foo of type Bool? and want to do if foo {...}. Should the condition evaluate to foo's nullability or truth? You expect it to check for thuth. I would say that such a mechanism should primarily check for nil, the way the cpp analogue does with pointers. Regardless, there's still a chance of a mass confusion being introduced to the language. You can always use optional's equality operator to explicitly express a comparison of the underlying objects or optional binding as a more general approach. In Swift, this isn't the case you would describe as 'beginner code'. It's common practice. The compiler can implicitly and safely convert non-optionals to optionals where needed, but not the other way around.

3 Likes

No, this is not correct. Optional chaining tries to a member access, subscript or function call if it called on something non-nil, otherwise it propagates a nil. It behaves either as a flatMap operator if the accessed field, subscript result or function result is optional, otherwise it behaves as simply a map operator.

struct S {
	let x: Int?
	let y: Int
}

let s: S? = S(x: 1, y: 2)
print(type(of: s?.x)) // => Optional<Int>, behaves like Optional.flatMap(_:)
print(type(of: s?.y)) // => Optional<Int>, behaves like Optional.map(_:)

note: Swift has an implicit promotion of T to T?, which blurs the lines between Optional.flatMap and Optional.map. From what I can tell, you can use Optional.flatMap everywhere you could use Optional.map, and get the same result.

@anthonylatsis It's probably true that the chances of if Bool? ever checking for nil in swift is exactly nil. I think that the requirement that Bool? needs to be compared to true and in a hidden fashion is unwrapped is confusing. My now learning that this is just an implementation detail is more confusing.

In objective-C checking the value of a nullableObject.isImportant returns false if nullableObject is indeed null.

Code like this in swift doesn't seem confusing:

if let realOne = one {
    if realOne.isImportant {
    }
}

Why should

if one?.isImportant

be confusing? It seems that this is a natural use of optional chaining.

I understand that this is common practice because that's the way the language works. I just don't really understand why.

Nitpick: it returns nil, not NO. They both have the same value with regards to the condition, but they’re different conceptually.

Here, you use Bool. Bool is easy to evaluate: it is either true or false. What about Bool? ? Is nil false? How about simply false? Should it be false, or rather true, since != nil? The confusion is in whether if Bool? should mean != nil or == true. Think about nesting optionals. Bool??? should naturally work as well then. That is the bottom line about the confusion and the arguable merits this brings. Personally, I do see this as becoming a classic trap if introduced, especially for Swift newcomers

Well, it simply has to be unwrapped for comparison reasons if there is a value. When you compare a value of type Bool? to true, this operator is called together with an implicit conversion of true to Optional.some(true) (The operands have to both be optionals).

The current behavior is the natural result of different language rules composing with each other. It would be reasonable to propose that a Bool?-typed result of an optional chain be allowed to be used directly as a condition, using the semantics that only .some(true) would pass the condition. However, that would be a special case in the language rules, and I imagine that a number of people would oppose it on those grounds. (I'm not expressing an opinion about it right now; I haven't really thought it through.)

4 Likes

As @John_McCall pointed out, there are two ways such a sugar for if optionalObject?.bool could work:

  1. if optionalObject.bool == Optional.some(true) { ...

    input evaluates to
    Optional.some(true) true
    Optional.some(false) false
    Optional.none false
  2. if optionalObject.bool != nil { ...

    input evaluates to
    Optional.some(true) true
    Optional.some(false) true
    Optional.none false

Essentially, there's contention over how Optional.some(false) should be treated. From a language design perspective, it's not immediately obvious which would be a better choice. Even worse, from a user perspective, it's not obvious what the language designers would have chosen.

There are existing constructs that make this distinction perfectly clear.

  1. if optionalObject?.bool ?? false
    This emulates #1 above, but makes it perfectly clear how to treat the nil case.
  2. if optionalObject?.bool == true
    This makes it perfectly clear that true is the only desired match. false and nil don't satisfy being equal to true.
1 Like

I'm not arguing for any changes, but I don't understand the contention that anyone would consider if someOpt?.boolProp { ... to be anything other than a stand-in for if someOpt?.boolProp == true { ....

3 Likes

That is probably the case, but it might have the potential to be confusing if the boolean is already phrased as a negative, e.g. one?.isNotImportant in the original example. It would also open the question as to whether you can negate it, use logical operators on it, use it in the ternary operator etc. Unless you're going to make the implicit conversion of Optional<Bool> to Bool uniform across the language, which probably isn't a good idea, then I don't see why it's a good idea in this specific context.

2 Likes

The alternative way of expressing it:

if let one = one, one.isImportant {
  ...
}

If we would push the topic about a better way unwrapping values in if/guard then we might be able to spell it like this (this uses a shorthand keyword that unwraps the value for the next body and the expressions followed after the comma):

if unwrap one, one.isImportant {
  ...
}
2 Likes

Maybe I am dense, but today, Swift supports the following:

  1. checking an optional for nil by explicit comparison: == nil or != nil.
  2. checking a boolean by explicit comparison: == true, != true, == false, or != false.
  3. checking a boolean by implicit comparison to true: if boolVar {...}, etc.

I don't understand why extending (3) to work with optional chaining would confuse anyone into thinking that an implicit comparison to nil was taking place. It's already the odd one out, as if Swift were completely consistent, it would force explicit comparisons, as must be done for nil checks.

I'm not sure which part of what I wrote you're responding to exactly, but there's no “implicit comparison to true” in 3. vs 2., there's just a boolean condition in either case. The explicit comparison to true just makes another boolean condition. It is in no way the “odd one out” and there's no consistency argument to be had here. The only consistency concern here, as I mention above, would be allowing implicit conversion from Optional<Bool> to Bool only in this very narrow case, rather than either generally or not at all.

1 Like

Sorry. We're talking past each other. I am writing from the perspective of someone writing the code, perhaps learning Swift as they go. Today, there's no way to write an implicit check for nil, so I don't understand why anyone would expect if optBool to start behaving that way. And by implicit, I mean that the comparison is omitted, because a Bool is itself a condition, as you wrote.

All that aside, I am not not advocating any change in this respect.

There is a third way I could imagine wanting to convert Bool? to Bool:
3. optionalObject.bool != false:

input evaluates to
Optional.some(true) true
Optional.some(false) false
Optional.none true