Skip to content

Commit dcea4c2

Browse files
committed
Implement tuple-only hashing with magic numbers; test hash properties
1 parent 2ca5404 commit dcea4c2

File tree

2 files changed

+52
-8
lines changed

2 files changed

+52
-8
lines changed

src/lens.jl

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ export constructorof
99
import Base: get, hash, ==
1010
using Base: getproperty
1111

12+
13+
# used for hashing
14+
function make_salt(s64::UInt64)::UInt
15+
if UInt === UInt64
16+
return s64
17+
else
18+
return UInt32(s64 >> 32) ^ UInt32(s64 & 0x00000000ffffffff)
19+
end
20+
end
21+
22+
1223
"""
1324
Lens
1425
@@ -119,7 +130,9 @@ end
119130
function ==(l1::ComposedLens{LO, LI}, l2::ComposedLens{LO, LI}) where {LO, LI}
120131
return l1.outer == l2.outer && l1.inner == l2.inner
121132
end
122-
hash(l::ComposedLens, h::UInt) = hash(l.outer, hash(l.inner, h))
133+
134+
const SALT_COMPOSEDLENS = make_salt(0xcf7322dcc2129a31)
135+
hash(l::ComposedLens, h::UInt) = hash(l.outer, hash(l.inner, SALT_INDEXLENS + h))
123136

124137
"""
125138
compose([lens₁, [lens₂, [lens₃, ...]]])
@@ -179,8 +192,11 @@ end
179192
struct IndexLens{I <: Tuple} <: Lens
180193
indices::I
181194
end
195+
182196
==(l1::IndexLens{I}, l2::IndexLens{I}) where {I} = l1.indices == l2.indices
183-
hash(l::IndexLens, h::UInt) = hash(l.indices, h)
197+
198+
const SALT_INDEXLENS = make_salt(0x8b4fd6f97c6aeed6)
199+
hash(l::IndexLens, h::UInt) = hash(l.indices, SALT_INDEXLENS + h)
184200

185201
Base.@propagate_inbounds function get(obj, l::IndexLens)
186202
getindex(obj, l.indices...)

test/test_core.jl

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,22 +206,50 @@ end
206206
end
207207

208208
@testset "equality & hashing" begin
209-
# singletons (identity and property lens) are object equal
210-
for (l, r) [
209+
# singletons (identity and property lens) are egal
210+
for (l1, l2) [
211211
@lens(_) => @lens(_),
212212
@lens(_.a) => @lens(_.a)
213213
]
214-
@test l === r
214+
@test l1 === l2
215+
@test l1 == l2
216+
@test hash(l1) == hash(l2)
215217
end
216218

217219
# composite and index lenses are structurally equal
218-
for (l, r) [
220+
for (l1, l2) [
219221
@lens(_[1]) => @lens(_[1])
220222
@lens(_.a[2]) => @lens(_.a[2])
223+
@lens(_.a.b[3]) => @lens(_.a.b[3])
221224
]
222-
@test l == r
223-
@test hash(l) == hash(r)
225+
@test l1 == l2
226+
@test hash(l1) == hash(l2)
224227
end
228+
229+
# inequality
230+
for (l1, l2) [
231+
@lens(_[1]) => @lens(_[2])
232+
@lens(_.a[1]) => @lens(_.a[2])
233+
@lens(_.a[1]) => @lens(_.b[1])
234+
]
235+
@test l1 != l2
236+
end
237+
238+
# Hash property: equality implies equal hashes, or in other terms:
239+
# lenses either have equal hashes or are unequal
240+
# Because collisions can occur theoretically (though unlikely), this is a property test,
241+
# not a unit test.
242+
random_lenses = (@lens(_.a[rand(Int)]) for _ in 1:1000)
243+
@test all((hash(l2) == hash(l1)) || (l1 != l2)
244+
for (l1, l2) in zip(random_lenses, random_lenses))
245+
246+
# Lenses should hash differently from the underlying tuples, to avoid confusion.
247+
# To account for potential collisions, we check that the property holds with high
248+
# probability.
249+
@test count(hash(@lens(_[i])) != hash((i,)) for i = 1:1000) > 900
250+
251+
# Same for tuples of tuples (√(1000) ≈ 32).
252+
@test count(hash(@lens(_[i][j])) != hash(((i,), (j,))) for i = 1:32, j = 1:32) > 900
225253
end
226254

227255

0 commit comments

Comments
 (0)