|
| 1 | +# Support for `nil` comparisons without `Equatable` conformance |
| 2 | + |
| 3 | +* Proposal: [SF-0035](0035-nil-comparisons-without-equatable.md) |
| 4 | +* Authors: [Matthew Turk](https://github.com/MatthewTurk247) |
| 5 | +* Review Manager: Jeremy S |
| 6 | +* Status: **Review: 2026-02-19...2026-01-25** |
| 7 | +* Bug: [swiftlang/swift-foundation#711](https://github.com/swiftlang/swift-foundation/issues/711) |
| 8 | +* Review: ([pitch](https://forums.swift.org/t/pitch-support-for-nil-comparisons-without-equatable-conformance/84684)) |
| 9 | + |
| 10 | +## Introduction/Motivation |
| 11 | + |
| 12 | +Foundation’s current implementation of `build_Equal(lhs:rhs:)` requires `Equatable` output conformance for its left- and right-hand expressions under all circumstances. As such, a declared predicate that compares a non-`Equatable` optional variable to `nil` does not compile. For example: |
| 13 | + |
| 14 | +```swift |
| 15 | +struct Message { |
| 16 | + struct Subject { |
| 17 | + let value: String |
| 18 | + } |
| 19 | + |
| 20 | + let subject: Subject? |
| 21 | +} |
| 22 | + |
| 23 | +let predicate = #Predicate<Message> { $0.subject == nil } |
| 24 | +// Referencing static method 'build_Equal(lhs:rhs:)' on 'Optional' requires that 'Message.Subject' conform to 'Equatable' |
| 25 | +``` |
| 26 | + |
| 27 | +This outcome is inconsistent with the semantics of the Swift standard library, which allows for optionals to be compared with `nil` regardless of whether the wrapped type conforms to `Equatable`. |
| 28 | + |
| 29 | +## Proposed solution and example |
| 30 | + |
| 31 | +To better align the `#Predicate` experience with that of Swift itself, I propose a new set of `build_Equal(lhs:rhs:)` and `build_NotEqual(lhs:rhs:)` overloads. These overloads cover the special cases of an expansion where either parameter is an instance of `NilLiteral`. The above example would leverage this one: |
| 32 | + |
| 33 | +```swift |
| 34 | +public static func build_Equal<LHS, Wrapped>( |
| 35 | + lhs: LHS, |
| 36 | + rhs: NilLiteral<Wrapped> |
| 37 | +) -> Equal<OptionalFlatMap<LHS, Wrapped, Value<Bool>, Bool>, Value<Bool?>> |
| 38 | +``` |
| 39 | + |
| 40 | +## Detailed design |
| 41 | + |
| 42 | +Each overload works by wrapping the non-`Equatable` variable expression in an `OptionalFlatMap`, whose initializer accepts a closure that can evaluate to different outputs based on whether a given input value is present. If a value is present, the closure should discard it and return `Value(true)`. Then, that optional result is compared to `nil` with the binary operator given by the name of the overload. This approach is analogous to using a branch of an `if let` conditional binding as a comparand, for which existing API does not constrain the input to `Equatable` types. |
| 43 | + |
| 44 | +```swift |
| 45 | +@available(FoundationPreview 6.4, *) |
| 46 | +extension PredicateExpressions { |
| 47 | + public static func build_Equal<LHS, Wrapped>( |
| 48 | + lhs: LHS, |
| 49 | + rhs: NilLiteral<Wrapped> |
| 50 | + ) -> Equal<OptionalFlatMap<LHS, Wrapped, Value<Bool>, Bool>, Value<Bool?>> |
| 51 | + |
| 52 | + public static func build_Equal<Wrapped, RHS>( |
| 53 | + lhs: NilLiteral<Wrapped>, |
| 54 | + rhs: RHS |
| 55 | + ) -> Equal<Value<Bool?>, OptionalFlatMap<RHS, Wrapped, Value<Bool>, Bool>> |
| 56 | + |
| 57 | + public static func build_NotEqual<LHS, Wrapped>( |
| 58 | + lhs: LHS, |
| 59 | + rhs: NilLiteral<Wrapped> |
| 60 | + ) -> NotEqual<OptionalFlatMap<LHS, Wrapped, Value<Bool>, Bool>, Value<Bool?>> |
| 61 | + |
| 62 | + public static func build_NotEqual<Wrapped, RHS>( |
| 63 | + lhs: NilLiteral<Wrapped>, |
| 64 | + rhs: RHS |
| 65 | + ) -> NotEqual<Value<Bool?>, OptionalFlatMap<RHS, Wrapped, Value<Bool>, Bool>> |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +With Swift’s method resolution, the compiler can choose the most specific overload available, so these will be preferred over the broader ones in ambiguous cases. |
| 70 | + |
| 71 | +## Impact on existing code |
| 72 | + |
| 73 | +Given that the overloads are purely additive and the macro will still generate the same source code as before, there should be no breaking changes. |
| 74 | + |
| 75 | +The new method resolution paths may, however, affect observed compiler performance, particularly for predicate code that already heavily relies on type inference. Below is a summary of compiling sample source files, using an SDK without these new overloads and using an SDK with them. |
| 76 | + |
| 77 | +| Number of predicates in file | Type check time without new overloads (s) | Type check time with new overloads (s) | |
| 78 | +|------------------------------|-------------------------------------------|----------------------------------------| |
| 79 | +| 10 | 6.073 | 6.135 | |
| 80 | +| 20 | 8.599 | 8.398 | |
| 81 | +| 30 | 11.129 | 11.466 | |
| 82 | +| 40 | 13.802 | 13.732 | |
| 83 | +| 50 | 16.266 | 16.199 | |
| 84 | +| 60 | 18.933 | 18.866 | |
| 85 | +| 70 | 21.265 | 21.458 | |
| 86 | +| 80 | 23.999 | 23.999 | |
| 87 | +| 90 | 26.466 | 26.599 | |
| 88 | +| 100 | 29.065 | 28.999 | |
| 89 | +| 110 | 31.599 | 31.666 | |
| 90 | +| 120 | 34.399 | 34.466 | |
| 91 | +| 130 | 37.266 | 36.665 | |
| 92 | +| 140 | 39.985 | 39.274 | |
| 93 | +| 150 | 42.437 | 42.099 | |
| 94 | +| 160 | 44.547 | 44.798 | |
| 95 | +| 170 | 47.298 | 47.282 | |
| 96 | +| 180 | 49.497 | 49.577 | |
| 97 | +| 190 | 52.414 | 52.251 | |
| 98 | +| 200 | 55.247 | 54.831 | |
| 99 | + |
| 100 | +At a glance, an impact on performance is not noticeable. |
| 101 | + |
| 102 | +## Alternatives considered |
| 103 | + |
| 104 | +### Relaxing requirements for existing `build_Equal(lhs:rhs:)` and `build_NotEqual(lhs:rhs:)` methods |
| 105 | + |
| 106 | +This would require relaxing several `Equatable` requirements elsewhere in the `#Predicate` infrastructure and public API. Such drastic changes could break existing evaluation functions or lead to other unforeseen consequences. |
| 107 | + |
| 108 | +### Introducing a new operator or expression type |
| 109 | + |
| 110 | +This could make the intent behind the API clearer. And with a dedicated operator or expression type, Foundation could support more operations for non-`Equatable` optionals down the line. However, that abstract prospect—for a use case that is already narrow—is unlikely to outweigh the drawbacks of maintaining a greatly expanded API surface, absent newfound technical justification or input from the open-source community. |
0 commit comments