Skip to content

Commit 2049714

Browse files
[Proposal] Support for nil comparisons without Equatable conformance (swiftlang#1735)
* [Proposal] Support for nil comparisons without Equatable conformance * Assign SF-0035 and update status --------- Co-authored-by: Jeremy Schonfeld <jschonfeld@apple.com>
1 parent b67da3c commit 2049714

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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

Comments
 (0)