Skip to content

Commit f018144

Browse files
committed
Add some documentation/concerns around Tuple
1 parent 9802943 commit f018144

File tree

2 files changed

+140
-5
lines changed

2 files changed

+140
-5
lines changed

Sources/FoundationDB/Tuple.swift

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ public protocol TupleElement: Sendable, Hashable, Equatable {
4949
}
5050

5151
// TODO: Make it a TypedTuple so that we don't have to typecast manually.
52+
/// A tuple represents an ordered collection of elements that can be encoded to and decoded from bytes.
53+
///
54+
/// Tuples can be used as keys in FoundationDB, and their encoding preserves lexicographic ordering.
55+
///
56+
/// ## Equality and Hashing
57+
///
58+
/// Tuple equality is based on the encoded byte representation of each element, which matches
59+
/// FoundationDB's tuple comparison semantics. This differs from Swift's native equality for
60+
/// floating-point values in the following ways:
61+
///
62+
/// - **Positive and negative zero**: `Tuple(0.0)` and `Tuple(-0.0)` are **not equal** because
63+
/// they have different bit patterns and encode to different bytes. This differs from Swift,
64+
/// where `0.0 == -0.0` is `true`.
65+
///
66+
/// - **NaN values**: `Tuple(Float.nan)` and `Tuple(Float.nan)` **are equal** if they have the
67+
/// same bit pattern, because they encode to the same bytes. This differs from Swift, where
68+
/// `Float.nan == Float.nan` is `false`.
69+
///
70+
/// These semantic differences ensure consistency with FoundationDB's tuple ordering and are
71+
/// important when using tuples as dictionary keys or in sets.
5272
public struct Tuple: Sendable, Hashable, Equatable {
5373
private let elements: [any TupleElement]
5474

@@ -126,8 +146,14 @@ public struct Tuple: Sendable, Hashable, Equatable {
126146
guard lhs.count == rhs.count else { return false }
127147

128148
for i in 0..<lhs.count {
129-
// Compare encoded bytes since Swift doesn't allow direct comparison of existential types.
130-
// This is semantically correct because the tuple encoding is canonical.
149+
// Swift's type system doesn't allow comparing `any Protocol` existentials directly,
150+
// even though TupleElement requires Equatable conformance. We compare encoded bytes
151+
// instead, which is semantically correct since tuple encoding is canonical:
152+
// equal values always produce equal encodings.
153+
//
154+
// Note: This means Float/Double comparison follows bit-pattern equality rather than
155+
// IEEE 754 equality (e.g., +0.0 and -0.0 are unequal, NaN values with the same bit
156+
// pattern are equal). See the Tuple documentation for details.
131157
if lhs.elements[i].encodeTuple() != rhs.elements[i].encodeTuple() {
132158
return false
133159
}
@@ -138,8 +164,9 @@ public struct Tuple: Sendable, Hashable, Equatable {
138164
public func hash(into hasher: inout Hasher) {
139165
hasher.combine(elements.count)
140166
for element in elements {
141-
// Hash encoded bytes since Swift doesn't allow direct hashing of existential types.
142-
// This ensures consistency with equality semantics.
167+
// Swift's type system doesn't allow hashing `any Protocol` existentials directly,
168+
// even though TupleElement requires Hashable conformance. We hash encoded bytes
169+
// instead, which ensures consistency with the equality implementation above.
143170
hasher.combine(element.encodeTuple())
144171
}
145172
}
@@ -155,11 +182,13 @@ struct TupleNil: TupleElement {
155182
}
156183

157184
static func == (lhs: TupleNil, rhs: TupleNil) -> Bool {
185+
// All TupleNil instances are equal (representing null/nil)
158186
return true
159187
}
160188

161189
func hash(into hasher: inout Hasher) {
162-
hasher.combine(0 as UInt8)
190+
// Use a constant value for consistency with the null type code
191+
hasher.combine(TupleTypeCode.null.rawValue)
163192
}
164193
}
165194

Tests/FoundationDBTests/FoundationDBTupleTests.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,109 @@ func tupleHashabilityDictionary() throws {
413413
#expect(dict[key2] == "Bob", "key2 value should be 'Bob'")
414414
#expect(dict[key3] == "Charlie", "key3 (same as key1) should retrieve 'Charlie'")
415415
}
416+
417+
// MARK: - Edge Cases
418+
419+
@Test("Tuple equality - Float positive and negative zero are unequal")
420+
func tupleFloatZeroInequality() throws {
421+
let tuple1 = Tuple(Float(0.0))
422+
let tuple2 = Tuple(Float(-0.0))
423+
424+
// Note: These are unequal because they have different bit patterns
425+
// and encode to different bytes. This differs from Swift's Float equality
426+
// where 0.0 == -0.0 is true.
427+
#expect(tuple1 != tuple2, "Positive and negative zero have different encodings")
428+
429+
// Verify they hash differently (important for Set/Dictionary correctness)
430+
#expect(tuple1.hashValue != tuple2.hashValue, "Different values must have potentially different hashes")
431+
}
432+
433+
@Test("Tuple equality - Double positive and negative zero are unequal")
434+
func tupleDoubleZeroInequality() throws {
435+
let tuple1 = Tuple(Double(0.0))
436+
let tuple2 = Tuple(Double(-0.0))
437+
438+
#expect(tuple1 != tuple2, "Positive and negative zero have different encodings")
439+
#expect(tuple1.hashValue != tuple2.hashValue, "Different values must have potentially different hashes")
440+
}
441+
442+
@Test("Tuple equality - Float NaN values are equal")
443+
func tupleFloatNaNEquality() throws {
444+
let tuple1 = Tuple(Float.nan)
445+
let tuple2 = Tuple(Float.nan)
446+
447+
// Note: These are equal because they encode to the same bytes
448+
// (same bit pattern). This differs from Swift's Float equality
449+
// where Float.nan == Float.nan is false.
450+
#expect(tuple1 == tuple2, "NaN values with same bit pattern encode to same bytes")
451+
#expect(tuple1.hashValue == tuple2.hashValue, "Equal values must have same hash")
452+
}
453+
454+
@Test("Tuple equality - Double NaN values are equal")
455+
func tupleDoubleNaNEquality() throws {
456+
let tuple1 = Tuple(Double.nan)
457+
let tuple2 = Tuple(Double.nan)
458+
459+
#expect(tuple1 == tuple2, "NaN values with same bit pattern encode to same bytes")
460+
#expect(tuple1.hashValue == tuple2.hashValue, "Equal values must have same hash")
461+
}
462+
463+
@Test("Tuple equality - Float infinity values")
464+
func tupleFloatInfinity() throws {
465+
let tuple1 = Tuple(Float.infinity)
466+
let tuple2 = Tuple(Float.infinity)
467+
let tuple3 = Tuple(-Float.infinity)
468+
469+
#expect(tuple1 == tuple2, "Same infinity values should be equal")
470+
#expect(tuple1 != tuple3, "Positive and negative infinity should be unequal")
471+
}
472+
473+
@Test("Tuple equality - Double infinity values")
474+
func tupleDoubleInfinity() throws {
475+
let tuple1 = Tuple(Double.infinity)
476+
let tuple2 = Tuple(Double.infinity)
477+
let tuple3 = Tuple(-Double.infinity)
478+
479+
#expect(tuple1 == tuple2, "Same infinity values should be equal")
480+
#expect(tuple1 != tuple3, "Positive and negative infinity should be unequal")
481+
}
482+
483+
@Test("Tuple equality - empty tuples")
484+
func tupleEmptyEquality() throws {
485+
let tuple1 = Tuple()
486+
let tuple2 = Tuple([])
487+
488+
#expect(tuple1 == tuple2, "Empty tuples should be equal")
489+
#expect(tuple1.hashValue == tuple2.hashValue, "Empty tuples should have same hash")
490+
#expect(tuple1.count == 0, "Empty tuple should have count 0")
491+
}
492+
493+
@Test("Tuple hashability - empty tuples in Set")
494+
func tupleEmptySet() throws {
495+
let tuple1 = Tuple()
496+
let tuple2 = Tuple([])
497+
498+
var set = Set<Tuple>()
499+
set.insert(tuple1)
500+
set.insert(tuple2)
501+
502+
#expect(set.count == 1, "Empty tuples should be deduplicated in Set")
503+
}
504+
505+
@Test("Tuple with nil values")
506+
func tupleWithNil() throws {
507+
let tuple1 = Tuple(TupleNil(), "hello", TupleNil())
508+
let tuple2 = Tuple(TupleNil(), "hello", TupleNil())
509+
510+
#expect(tuple1 == tuple2, "Tuples with nil values should be equal")
511+
#expect(tuple1.hashValue == tuple2.hashValue, "Tuples with nil values should have same hash")
512+
#expect(tuple1.count == 3, "Tuple should have 3 elements including nils")
513+
}
514+
515+
@Test("Tuple equality - nil values in different positions are unequal")
516+
func tupleNilPositions() throws {
517+
let tuple1 = Tuple(TupleNil(), "hello")
518+
let tuple2 = Tuple("hello", TupleNil())
519+
520+
#expect(tuple1 != tuple2, "Tuples with nils in different positions should be unequal")
521+
}

0 commit comments

Comments
 (0)