Optional comparison revisited

Why can't we compare optionals?

``````var x: Int?
x == 0 // ok
x > 0  // error
``````

I found some explanation here (0121-remove-optional-comparison-operators):

but I fail to see why this example:

``````struct Pet {
let age: Int
}

struct Person {
let name: String
let pet: Pet?
}

let peeps = [
Person(name: "Fred", pet: Pet(age: 5)),
Person(name: "Jill", pet: .None), // no pet here
Person(name: "Burt", pet: Pet(age: 10)),
]

let ps = peeps.filter { \$0.pet?.age < 6 }

ps == [Fred, Jill] // if you donāt own a pet, your non-existent pet is considered to be younger than any actual pet  š¶
``````

says "Fred, Jill" is returned (was it some gotcha of the previous implementation?). I would suppose that if `\$0.pet?.age < 6` was allowed it should have returned "false" for Jill, as if it was written:

`````` `\$0.pet?.age != nil && \$0.pet!.age < 6` // or the better "if let" form
``````

so the expected result would be [Fred].

Obviously instead of the currently disallowed `x > 0` I can do:

``````x != nil && x! > 0
``````

or its better "if let..." form. Or, at least conceptually, I can do "the equivalent":

``````x == 1 && x == 2 && x == 3 ..... && x == .max
``````

It just looks odd that `==` and `!=` is allowed but `<` `>` not. Perhaps I am missing some good example where optional comparison would be really surprising?

Note that the motivation section of the quoted article refers to some limitations in generics system which might be no longer applicable today.

The problem with making `Optional` `Comparable` is not so much in a single comparison, but the cascading effects of sorting algorithms. As an exercise, make `Optional` `Comparable` and see if you can get something that looks correct out of this:

``````[2, nil, 1].sorted()
``````

Here's `==` for `Optional`, for inspiration.

``````public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l == r
case (nil, nil):
return true
default:
return false
}
}
``````

How does `.none` compare against `.some`?

3 Likes

If `nil < 42` was `false` but `42 < nil` was also `false`, sorting an array like `[3, 2, nil, 7, 1]` would result in `[2, 3, nil, 1, 7]`, which doesn't make much sense. Consider also that `Equatable` and `Comparable` are completely different protocols, with different semantic requirements, which assume a different underlying mathematical structure: `Comparable` "includes" `Equatable` in the definition of its semantics (so the former implies the existence of the latter), but the inverse is not true, so there's nothing particularly weird in allowing `42 != nil` but not `42 < nil` (while if we allowed `42 < nil` we would be forced to also allow `42 != nil`).

3 Likes

This is already how `NaN` works. The documentation for `Comparable` explicitly states that "a conforming type may contain a subset of values which are treated as exceptional" that donāt take part in the strict total order. Personally, I think this is paradoxical to the point of nonsense, but given that it is how the protocol is defined, I donāt see why treating `FloatingPoint.nan` is really that different from `Optional.none`. They represent the same concept ā the absence of a value.

2 Likes

`.none` isn't an exceptional value for an Optional. It's arguably the entire point of the type.

10 Likes

My take here is that `.nan` is an exception that should not be repeated nor followed as a foundational example: twice the nonsense doesn't produce "sense"

I donāt know. Given the carve-out for `.nan`, `Comparable` is already useless in practice for anyone who needs an actual strict total order on an arbitrary `T`.

Itās perfectly reasonable to give an arbitrary ordering for `.some` and `.none`. Swift does this automatically for other enums.

Swift used to have a `<` implementation for `Optional`, but it was removed in SE-0121.

The motivation in that proposal is mainly driven by Swiftās implicit optional wrapping, causing surprising behavior. It also mentions that these operators could be re-introduced once conditional conformance is available (though I find the actual rationale for this elusiveā¦ I am not really clear why lack of `Comparable` conformance makes or breaks these operators).

Conditional conformance is now available, but the implicit wrapping issue remains. Disallowing it was proposed as SE-0123. I suspect that this topic is worth revisiting ā in particular, the similar implicit conversion to `Any` remains a blocker for defining `<` for `any Comparable`. Perhaps some opt-in to forbidding conversion would be worthwhile.

3 Likes

`.nan` probably exists because computation on real machines is not like ideal mathematics: it's a necessary evil, that one could still avoid with careful design. But the fact that it exists doesn't mean that we should then encourage non-strict semantics in any other arbitrary case. `.some` and `.none` are perfectly valid values for an instance of type `Optional`, without exceptions, and if the type conformed to `Comparable` it would mean that we should come up with a sensible definition of an ordering for them: for example, I'd expect that if `nil` is lower than `42`, then `42` is greater than `nil`.

In almost all project that I work on I have the old Standard Library implementation.

The problem with this is the conflation of concepts that occurs because of `<` being a symbol instead of a name, and only returning a `Bool`.

If `<` means "is less than", then all four `Comparable` requirements should return `false`, when one of the operands is `nil`. `nil` is neither less than, greater than, nor equal to a value.

But if `<` means "is ordered before", then `nil` could be defined to come before or after all values.

The problem is, `<` means both of those things. So it's only appropriate to apply it to types where both wordings hold. `Optional` can only mean the latter.

I don't think your distinction holds for String. Character ordering is defined as "comes before" or "comes after". There is no inherent value to a character such that it is "less than" or "greater than" some other character.

It seems that your requirement could only ever apply to ordinals. Any other type is using the "before/after" semantic.

2 Likes

Yeah, seems like the idea needs work. What about, "`<` cannot apply to types that are synonymous with their primary associated type (that's not the correct terminology for the generic placeholder but I don't know if there's another wording that matches the protocol equivalent), and do not always represent a value of that type"? What else but `Optional` fits that description? Property wrappers are what come to mind, but do those cause any related problems?

This table presents all sane possibilities how comparing some to none can work:

``````some>none   some<none   none>none   none<none   comment
false       false       false       false       // as Float.nan
true        false       false       false       // "something is more than nothing"
false       true        false       false       // "something is less than nothing"
nil         nil         nil         nil         // Optional<Boolean>
``````

`Float.nan` would be "compatible" with Float's `nan`, "something is more/less and than nothing" would give some reasonable sort order, although strictly speaking it's not necessary. `Optional<Boolean>` (returning nil) is perhaps the most "safe" option (although it won't meet `Comparable` conformance that needs bare `Bool`).

ā® As Ben has pointed out above, "something is more/less than nothing" is the behaviour we have with "normal" enum types:

``````enum MyOptional1: Comparable {
case none
case some(Int)
}

enum MyOptional2: Comparable {
case some(Int)
case none
}

MyOptional1.some(0) > .none // true
MyOptional1.some(0) < .none // false
MyOptional1.none > .none    // false
MyOptional1.none < .none    // false

MyOptional2.some(0) > .none // false
MyOptional2.some(0) < .none // true
MyOptional2.none > .none    // false
MyOptional2.none < .none    // false
``````

I would agree, except for that synonymousness I mentioned. `none` compared to `some` has different semantics than `.none` being compared against a `Wrapped`, and there's not enough baked into the four `Comparable` operators to deal with that.

``````let i = 1
i is Int // true
i is Optional<Int> // true
``````

I think the problem may be unique to `Optional`, at least until property wrappers support `get throws` or something else that could allow them to mimic a similar effect.

Key to how `Optional` contributes to the correctness of your code is that you are prompted to consider the `nil` case. When thereās both implicit optional wrapping and comparison operators for `Optional`, you can sort or ask for the minimum and, not realizing that one operand is optional, get a `nil` result. This is not desired.

As @Ben_Cohen writes above, an alternative would be to disable implicit optional wrapping in this scenario, which was considered and rejected in SE-0123. Another alternative is to have a comparison operator that makes allowance for an unordered result (i.e., partial orderingāe.g., a spaceship operator `<=>` that returns an optional ternary `Optional<ComparisonResult>`), which has the nice property that it wouldnāt require an exception for NaN.

These (and variations on each, such as returning `Bool?` for `<`, etc.) are pretty much the only possibilities in this solution space. Note that in all of these cases, you would be required to handle the comparison-to-`nil` case explicitly in some way: currently, by unwrapping the optional value; in the case of Benās alternative, by wrapping the non-optional value; and in the case of a comparison operator that allows for partial order, handling the unordered case. This is actively desirable.

It is semantically the case that two values that may be equal or unequal can have no defined order relative to each other. Thatās not odd, and there is no alternative design that can make this fact go away any more than you can make Ļ = 3.

4 Likes

If I recall, it was because a type `T` that had no `Comparable` conformance would nonetheless have values that were apparently comparable by implicit promotion to `T?`āalthough in the moment I canāt recall how the comparison of two `.some` cases was handled back then.

SE-123ās rejection rationale suggests the path forward:

In the post-Swift 3 timeframe, we can investigate other broader options for solving this problem more generally (e.g. a new parameter attribute).

So you could then define e.g.:

``````func < (lhs: @explicit any Comparable, rhs: @explicit any Comparable) -> Bool {
// define total ordering of all Comparable types
}
``````

But without the current problem, which is that if you define this, it could implicitly compare `String` to `Int` without you realizing, which would be pretty terrible.

(Vaguely relatedly, we should consider revisiting that `f(arg: any Comparable)` is more favored than `f(arg: some Comparable)`, which makes the above problem even worse)

With this functionality, it should hopefully be OK to reintroduce `<` for optionals and also make them conditionally `Comparable`.

1 Like

@tera I have a pretty decent answer on StackOverflow about precisely this. I'll cross-post it here:

It makes perfect sense for the equality operator to support optionals, because it's absolutely clear that for any integer valued variable `i`:

• `nil == nil`
• `nil != i`
• `i != nil`
• `i == i` if and only if their values are the same

On the other hand, it's not clear how comparison to `nil` should act:

Is `i` less than `nil`?

• If I want to sort an array so that all the `nil`s come out at the end, then I would want `i` to be less than `nil`.
• But if I want to sort an array so that all the `nil`s come out at the start, then I would want `i` to be greater than `nil`.

Since either of these are equally valid, it wouldn't make sense for the standard library to favor one over the other. It's left to the programmer to implement whichever comparison makes sense for their use-case.

Here's a toy implementation that generates a comparison operator to suite either case:

``````func nilComparator<T: Comparable>(nilIsLess: Bool) -> (T?, T?) -> Bool {
return {
switch (\$0, \$1) {
case (nil, nil): return false
case (nil, _?): return nilIsLess
case (_?, nil): return !nilIsLess
case let (a?, b?): return a < b
}
}
}

let input = (0...10).enumerated().map {
\$0.offset.isMultiple(of: 2) ? Optional(\$0.element) : nil
}

func concisePrint<T>(_ optionals: [T?]) -> String {
return "[" + optionals.map { \$0.map{ "\(\$0)?" } ?? "nil" }.joined(separator: ", ") + "]"
}

print("Input:", concisePrint(input))
print("nil is less:", concisePrint(input.sorted(by: nilComparator(nilIsLess: true))))
print("nil is more:", concisePrint(input.sorted(by: nilComparator(nilIsLess: false))))
``````

Output:

Input: `[0?, nil, 2?, nil, 4?, nil, 6?, nil, 8?, nil, 10?]`

nil is less: `[nil, nil, nil, nil, nil, 0?, 2?, 4?, 6?, 8?, 10?]`

nil is more: `[0?, 2?, 4?, 6?, 8?, 10?, nil, nil, nil, nil, nil]`