Intro
This isn't an evolution proposal; it's my research on what feels like a weird snaggy bit around bool comparisons in Swift involving three-value logic. The snag makes it too easy to write fragile or incorrect code.
I'm interested in ways to make this fragile code harder to write, or at least have it signposted better when you're doing it (warnings etc).
This has been discussed before, see forum links at end.
The issue
Swift has extensions/code for ==
that allow comparison of Optional<Bool>
with Bool
, essentially sneaking in a kind of three-value logic. Not sure of the historical reasons for this, but it allows various mistakes and fragile code to be created without any compiler warning.
Example: in order to check if an optional string has data -- in which we need it to be non-nil and .isEmpty == false
-- we would idiomatically write:
let str: String? = ...
if let str, !str.isEmpty {
However, I've seen this being done and even promoted as 'nicer' code:
if str?.isEmpty == false {
This works as per desired behaviour, but it's not good code; it's fragile. For example, someone might come along later and want to code the opposite check ("it's nil or .isEmpty == true
"), and quite understandably might just flip the boolean:
if str?.isEmpty == true {
This is not actually the opposite check -- it's buggy, it will regard nil
as containing data (because neither true
nor false
are equal to nil
).
So IMO there's too much cognitive load to reason about/change this code, and you're expected to know false != nil
to fully understand it.
(Note also that if someone flipped the code by changing the check to != false
instead of == true
, it would be a correct change! Which is very counter-intuitive.)
Here's another example of broken code I've seen in the wild: it's supposed to be guarding against an array having true
at the end:
let bools: [Bool] = []
guard bools.last == false else {
print("Error: last bool in array is true")
}
So .last
returns an optional, and nil != false
, so if array is empty we enter the guard.
(And there's no compiler warning for the code above.)
Bool comparisons considered harmful?
Thought:
Is there ever an actual need to write == true
or == false
in Swift? It's not idiomatic, and crucially the ability to do so allows the sorts of fragility detailed above to happen.
Things that might help
Sometimes people like explicit bool checks for clarity (even though it's not idiomatic) -- in those cases, extension on Bool
could provide .isTrue
and .isFalse
(and no handling implemented for Optional<Bool>
).
SwiftLint has a rule to warn you off creating option Bool properties (discouraged_optional_boolean Reference). This is only heading off one possible cause of the issue though; optional chaining and funcs that return optional bool (e.g. [Bool].last
) can still introduce opportunity for the problem without any compiler warning AFAICS.
We can write a specialisation on ==
that generates a warning, at least:
@available(*, deprecated, message: "Comparing Optional<Boolean> using == is not safe practice. Unwrap the value and use direct expression evaluation e.g. 'if boolExpression' or 'if !boolExpression'")
func == (lhs: Bool?, rhs: Bool) -> Bool {
guard let unwrappedLHS = lhs else { return false }
return unwrappedLHS == rhs
}
// (not shown: other two variants for `Bool, Bool?` and `Bool?, Bool?`)
and this code does generate compiler warnings for me for these two problem cases:
let strings: [String] = []
if strings.last?.isEmpty == false { }
let str: String? = nil
if str?.isEmpty == false { }
Links
There have been a few related proposal for SwiftLint rules that never panned out:
- Rule Request: Discourage comparing Bool value with `true` or `false` · Issue #3420 · realm/SwiftLint · GitHub
- Rule request: No explicit `== true` or `== false` · Issue #1502 · realm/SwiftLint · GitHub
Related forum posts:
- The Optional Truth
- Boolean comparison causes extremely slow compilation -- another reason to avoid explicit boolean comparisons