Skip to content

Commit d8938be

Browse files
authored
Merge pull request #162 from phipsgabler/phg/hashing
Fix #161: Implement == and hash for IndexLens and ComposedLens
2 parents 3ce508d + 4d22048 commit d8938be

File tree

3 files changed

+74
-2
lines changed

3 files changed

+74
-2
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Setfield"
22
uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46"
3-
version = "0.7.1"
3+
version = "0.8.0"
44

55
[deps]
66
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"

src/lens.jl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ export setproperties
66
export constructorof
77

88

9-
import Base: get
9+
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
@@ -99,6 +110,7 @@ struct IdentityLens <: Lens end
99110
get(obj, ::IdentityLens) = obj
100111
set(obj, ::IdentityLens, val) = val
101112

113+
102114
struct PropertyLens{fieldname} <: Lens end
103115

104116
function get(obj, l::PropertyLens{field}) where {field}
@@ -115,6 +127,13 @@ struct ComposedLens{LO, LI} <: Lens
115127
inner::LI
116128
end
117129

130+
function ==(l1::ComposedLens{LO, LI}, l2::ComposedLens{LO, LI}) where {LO, LI}
131+
return l1.outer == l2.outer && l1.inner == l2.inner
132+
end
133+
134+
const SALT_COMPOSEDLENS = make_salt(0xcf7322dcc2129a31)
135+
hash(l::ComposedLens, h::UInt) = hash(l.outer, hash(l.inner, SALT_INDEXLENS + h))
136+
118137
"""
119138
compose([lens₁, [lens₂, [lens₃, ...]]])
120139
@@ -174,6 +193,11 @@ struct IndexLens{I <: Tuple} <: Lens
174193
indices::I
175194
end
176195

196+
==(l1::IndexLens{I}, l2::IndexLens{I}) where {I} = l1.indices == l2.indices
197+
198+
const SALT_INDEXLENS = make_salt(0x8b4fd6f97c6aeed6)
199+
hash(l::IndexLens, h::UInt) = hash(l.indices, SALT_INDEXLENS + h)
200+
177201
Base.@propagate_inbounds function get(obj, l::IndexLens)
178202
getindex(obj, l.indices...)
179203
end

test/test_core.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,54 @@ end
205205
end
206206
end
207207

208+
@testset "equality & hashing" begin
209+
# singletons (identity and property lens) are egal
210+
for (l1, l2) [
211+
@lens(_) => @lens(_),
212+
@lens(_.a) => @lens(_.a)
213+
]
214+
@test l1 === l2
215+
@test l1 == l2
216+
@test hash(l1) == hash(l2)
217+
end
218+
219+
# composite and index lenses are structurally equal
220+
for (l1, l2) [
221+
@lens(_[1]) => @lens(_[1])
222+
@lens(_.a[2]) => @lens(_.a[2])
223+
@lens(_.a.b[3]) => @lens(_.a.b[3])
224+
]
225+
@test l1 == l2
226+
@test hash(l1) == hash(l2)
227+
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
253+
end
254+
255+
208256
@testset "type stability" begin
209257
o1 = 2
210258
o22 = 2

0 commit comments

Comments
 (0)