Permalink
Find file
6ee1fef Jan 16, 2017
@benrimmington @jtbandes @lattner @saagarjha @krilnon @addisonleong
122 lines (85 sloc) 5.88 KB

Remove Optional Comparison Operators

Introduction

Swift's Comparable protocol requires 4 operators, <, <=, >, and >=, beyond the requirements of Equatable.

The standard library additionally defines the following 4 variants, which accept operands of Optional type, with the semantics that .none < .some(_):

public func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func <= <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func >= <T : Comparable>(lhs: T?, rhs: T?) -> Bool

This proposal removes the above 4 functions.

swift-evolution discussion threads:

Motivation

These optional-friendly comparison operators exist to provide an ordering between optional and non-optional values of the same (Comparable) type. Ostensibly such a feature would be useful in generic programming, allowing algorithms written for Comparable values to be used with optionals:

[3, nil, 1, 2].sorted()  // returns [nil, 1, 2, 3]

However, this doesn't work in current versions of Swift, because generics don't support conditional conformances like extension Optional: Comparable where Wrapped: Comparable, so Optional is not actually Comparable.

The most common uses of these operators involve coercion or promotion from non-optional to optional types, such as:

let a: Int? = 4
let b: Int = 5
a < b  // b is coerced from "Int" to "Int?" to match the parameter type.

SE-0123 seeks to remove this coercion (for arguments to operators) for a variety of reasons.

If the coercion is not removed (if no change is made), the results of comparisons with Optional values are sometimes surprising, making it easy to write bugs. In a thread from December 2015, Al Skipp offers the following 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  🐶

On the other hand, if coercion is removed for operator arguments, callers will be required to explicitly handle mixtures of optional and non-optional values in their code, which reduces the "surprise factor":

let a: Int? = 4
let b: Int = 5
a < b            // no longer works
a < .some(b)     // works
a < Optional(b)  // works

In either case, what remains is to decide whether these semantics (that nil is "less than" any non-nil value) are actually useful and worth keeping. Until generics are more mature, the issue of Optional being conditionally Comparable can't be fully discussed/implemented, so it makes the most sense to remove these questionably-useful operators for now (a breaking change for Swift 3), and add them back in the future if desired.

Proposed solution

Remove the versions of <, <=, >, and >= which accept optional operands.

Variants of == and != which accept optional operands are still useful, and their results unsurprising, so they will remain.

(In the future, once it is possible for Optional to conditionally conform to Comparable, it may make sense to reintroduce these operators by adding such a conformance.)

Impact on existing code

Code which compares optional values:

let a: Int?
let b: Int
if a < b { ... }        // if coercion remains
if a < .some(b) { ... } // if coercion is removed

will need to be updated to explicitly unwrap the values before comparing:

if let a = a where a < b { ... }
// or
guard let a = a else { ... }
if a < b { ... }
// or
if a! < b { ... }

This impact is potentially severe, however it may reveal previously-subtle bugs in user code. (The severity will also be somewhat mitigated if optional coercion is removed, since those changes will affect all the same call sites.)

Fix-it hints for adding ! are already provided when optional values are passed to non-optional parameters. However, this would significantly change the meaning of user code: a! < b may trap where a < b would have previously returned false. At the core team's discretion, deprecating the functions (with a helpful message) before removing them may be the best course of action.

Alternatives considered

The alternative is to keep these operators as they are. As discussed above, this leaves the potential for surprising results, and the fact remains that removing them after Swift 3 would break source stability (while reintroducing them later would be purely additive).