Skip to content

Commit 2e38117

Browse files
authored
Merge pull request #7 from jw3126/hash_eq_as
Add hash_eq_as
2 parents ee09c8c + 2223432 commit 2e38117

File tree

3 files changed

+134
-10
lines changed

3 files changed

+134
-10
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "StructHelpers"
22
uuid = "4093c41a-2008-41fd-82b8-e3f9d02b504f"
33
authors = ["Jan Weidner <jw3126@gmail.com> and contributors"]
4-
version = "1.0"
4+
version = "1.1"
55

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

src/StructHelpers.jl

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,44 @@ export @enumbatteries
55

66
import ConstructionBase: getproperties, constructorof, setproperties
77

8+
"""
9+
hash_eq_as(obj)
10+
11+
This allows to fine tune the behavior or `hash`, `==` and `isequal` for structs decorated by [`@batteries`](@ref).
12+
For instances the generated `isequal` method looks like this:
13+
```julia
14+
function Base.isequal(o1::T, o2::T)
15+
proxy1 = StructHelpers.hash_eq_as(o1)
16+
proxy2 = StructHelpers.hash_eq_as(o2)
17+
isequal(proxy1, proxy2)
18+
end
19+
```
20+
Overloading `hash_eq_as` is useful for instance if you want to skip certain fields
21+
of `obj` or handle them in a special way.
22+
"""
23+
function hash_eq_as(obj)
24+
# it would be better to just use getproperties
25+
# but this would cause hashed to change, which we want to
26+
# keep backwards compatible for now.
27+
#
28+
# TODO: Change to getproperties once we want to make a hash breaking change
29+
Tuple(getproperties(obj))
30+
end
31+
832
@inline function structural_eq(o1, o2)
933
getproperties(o1) == getproperties(o2)
1034
end
1135
@inline function structural_isequal(o1, o2)
1236
isequal(getproperties(o1), getproperties(o2))
1337
end
1438

15-
start_hash(o, h, typesalt::Nothing) = Base.hash(typeof(o), h)
16-
start_hash(o, h, typesalt) = Base.hash(typesalt, h)
39+
function start_hash(o, h, typesalt::Nothing)
40+
Base.hash(typeof(o), h)
41+
end
42+
function start_hash(o, h, typesalt)
43+
Base.hash(typesalt, h)
44+
end
45+
1746
@inline function structural_hash(o, h::UInt, typesalt=nothing)::UInt
1847
h = start_hash(o, h, typesalt)
1948
nt = Tuple(getproperties(o))
@@ -64,7 +93,7 @@ const BATTERIES_DOCSTRINGS = (
6493
kwshow = "Overload `Base.show` such that the names of each field are printed.",
6594
getproperties = "Overload `ConstructionBase.getproperties`.",
6695
constructorof = "Overload `ConstructionBase.constructorof`.",
67-
typesalt = "Only used if `hash=true`. In this case the `hash` will be purely computed from `typesalt` and `getproperties(T)`. The type `T` will not be used otherwise. This makes the hash more likely to stay constant, when executing on a different machine or julia version",
96+
typesalt = "Only used if `hash=true`. In this case the `hash` will be purely computed from `typesalt` and `hash_eq_as(obj)`. The type `T` will not be used otherwise. This makes the hash more likely to stay constant, when executing on a different machine or julia version",
6897
)
6998

7099
if (keys(BATTERIES_DEFAULTS) != keys(BATTERIES_DOCSTRINGS))
@@ -107,6 +136,8 @@ end
107136
Supported options and defaults are:
108137
109138
$(doc_batteries_options())
139+
140+
See also [`hash_eq_as`](@ref)
110141
"""
111142
macro batteries(T, kw...)
112143
nt = parse_all_macro_kw(kw)
@@ -149,15 +180,26 @@ macro batteries(T, kw...)
149180
fieldnames = Base.fieldnames(Base.eval(__module__, T))
150181
end
151182
if nt.hash
152-
def = :(Base.hash(o::$T, h::UInt) = $(structural_hash)(o,h, $(nt.typesalt)))
183+
def = :(function Base.hash(o::$T, h::UInt)
184+
h = ($start_hash)(o, h, $(nt.typesalt))
185+
proxy = ($hash_eq_as)(o)
186+
Base.hash(proxy, h)
187+
end
188+
)
153189
push!(ret.args, def)
154190
end
155191
if nt.eq
156-
def = :(Base.:(==)(o1::$T, o2::$T) = $(structural_eq)(o1, o2))
192+
def = :(function Base.:(==)(o1::$T, o2::$T)
193+
($hash_eq_as)(o1) == ($hash_eq_as)(o2)
194+
end
195+
)
157196
push!(ret.args, def)
158197
end
159198
if nt.isequal
160-
def = :(Base.isequal(o1::$T, o2::$T) = $(structural_isequal)(o1, o2))
199+
def = :(function Base.isequal(o1::$T, o2::$T)
200+
isequal($hash_eq_as(o1), $hash_eq_as(o2))
201+
end
202+
)
161203
push!(ret.args, def)
162204
end
163205
if nt.kwshow

test/runtests.jl

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ struct Salt2 end
3636
struct NoSalt end
3737
@batteries NoSalt
3838

39+
struct SaltABC; a;b;c end
40+
@batteries SaltABC typesalt = 1
41+
3942
struct SErrors;a;b;c;end
4043

4144
struct NoSelfCtor; a; end
@@ -91,9 +94,21 @@ struct SNoIsEqual; a; end
9194
@test_throws Exception @macroexpand @batteries SErrors nonsense=true
9295
@macroexpand @batteries SErrors kwconstructor=true
9396

94-
@test hash(Salt1()) === hash(Salt1b())
95-
@test hash(Salt1()) != hash(NoSalt())
96-
@test hash(Salt1()) != hash(Salt2())
97+
@testset "typesalt" begin
98+
@test hash(Salt1()) === hash(Salt1b())
99+
@test hash(Salt1()) != hash(NoSalt())
100+
@test hash(Salt1()) != hash(Salt2())
101+
102+
# persistence
103+
@test hash(Salt1()) === 0xd39a1e58a7b0c35e
104+
@test hash(Salt1b()) === 0xd39a1e58a7b0c35e
105+
@test hash(Salt2()) === 0x2f64a52e5f45d104
106+
107+
@test hash(SaltABC(1 , 2 , 3 )) === 0x92290cfd972fe54d
108+
@test hash(SaltABC(10 , 2 , 3 )) === 0xcc48b9e98b6f3ef4
109+
@test hash(SaltABC(10 , 20 , 3 )) === 0x6f8c614051f68ec7
110+
@test hash(SaltABC(10 , 20 , 30)) === 0x90cb2b9a94741e53
111+
end
97112

98113
@test WithSelfCtor(WithSelfCtor(1)) === WithSelfCtor(1)
99114
@test NoSelfCtor(NoSelfCtor(1)) != NoSelfCtor(1)
@@ -153,3 +168,70 @@ struct Bad end
153168
@test_throws "Bad keyword argument value" @macroexpand @batteries Bad hash=:nonsense
154169
end
155170
end
171+
172+
abstract type AbstractHashEqAs end
173+
function SH.hash_eq_as(x::AbstractHashEqAs)
174+
return x.hash_eq_as(x.payload)
175+
end
176+
177+
struct HashEqAs <: AbstractHashEqAs
178+
hash_eq_as
179+
payload
180+
end
181+
SH.@batteries HashEqAs
182+
struct HashEqAsTS1 <: AbstractHashEqAs
183+
hash_eq_as
184+
payload
185+
end
186+
SH.@batteries HashEqAsTS1 typesalt = 1
187+
188+
struct HashEqAsTS1b <: AbstractHashEqAs
189+
hash_eq_as
190+
payload
191+
end
192+
SH.@batteries HashEqAsTS1b typesalt = 1
193+
194+
struct HashEqAsTS2 <: AbstractHashEqAs
195+
hash_eq_as
196+
payload
197+
end
198+
SH.@batteries HashEqAsTS2 typesalt = 2
199+
200+
@testset "hash_eq_as" begin
201+
@test HashEqAs(identity, 1) != HashEqAs(identity, -1)
202+
@test HashEqAs(abs, 1) == HashEqAs(abs, -1)
203+
@test isequal(HashEqAs(identity, 1), HashEqAs(x->x, 1))
204+
205+
@test hash(HashEqAs(identity, 1)) != hash(HashEqAs(identity, -1))
206+
@test hash(HashEqAs(abs, 1)) === hash(HashEqAs(abs, -1))
207+
@test hash(HashEqAs(identity, 1)) === hash(HashEqAs(x->x, 1))
208+
209+
@test hash(HashEqAsTS1(identity, 1)) != hash(HashEqAsTS1(identity, -1))
210+
@test hash(HashEqAsTS1(abs, 1)) == hash(HashEqAsTS1(abs, -1))
211+
@test hash(HashEqAsTS1b(abs, 1)) == hash(HashEqAsTS1(abs, -1))
212+
@test hash(HashEqAsTS2(abs, 1)) != hash(HashEqAsTS1(abs, -1))
213+
214+
@test hash(HashEqAsTS1(x->2x::Int, 1)) === hash(HashEqAsTS1(identity, 2))
215+
@test hash(HashEqAsTS2(x->2x::Int, 1)) != hash(HashEqAsTS1(identity, 2))
216+
@test hash(HashEqAsTS1(identity, 1)) === 0x486b072c90d60e64
217+
@test hash(HashEqAsTS2(x->5x, 1)) === 0xa4360acf486c15a4
218+
end
219+
220+
mutable struct HashEqErr
221+
a
222+
b
223+
end
224+
Base.hash(::HashEqErr, h::UInt) = error()
225+
Base.isequal(::HashEqErr, ::HashEqErr) = error()
226+
Base.:(==)(::HashEqErr, ::HashEqErr) = error()
227+
228+
@testset "structural hash eq" begin
229+
S = HashEqErr
230+
@test SH.structural_eq(S(1,3), S(1,3))
231+
@test !SH.structural_eq(S(1,NaN), S(1,NaN))
232+
@test SH.structural_isequal(S(1,NaN), S(1,NaN))
233+
@test !SH.structural_isequal(S(2,NaN), S(1,NaN))
234+
@test SH.structural_hash(S(2,NaN), UInt(0)) != SH.structural_hash(S(1,NaN), UInt(0))
235+
@test SH.structural_hash(S(2,NaN), UInt(0)) == SH.structural_hash(S(2,NaN), UInt(0))
236+
@test SH.structural_hash(S(2,NaN), UInt(0)) != SH.structural_hash(S(2,NaN), UInt(1))
237+
end

0 commit comments

Comments
 (0)