install like
using Pkg
pkg"add StructEquality"
load like
using StructEquality
which let's you easily define hash
and ==
for your custom struct.
@struct_hash_equal struct MyStruct
one
two
end
MyStruct("1", [2]) == MyStruct("1", [2]) # true
macro | defines ... for your struct |
---|---|
@struct_hash |
hash |
@struct_equal |
== |
@struct_isequal |
isequal |
@struct_isapprox |
isapprox |
combined macro | defines ... for your struct |
---|---|
@struct_hash_equal |
hash , == |
@struct_hash_equal_isapprox |
hash , == , isapprox |
@struct_hash_equal_isequal |
hash , == , isequal |
@struct_hash_equal_isequal_isapprox |
hash , == , isequal , isapprox |
If you don't like macros, you can directly use the underlying generated functions and implement the definitions yourself.
generated functions | use for custom implementation like ... |
---|---|
struct_hash |
Base.hash(a::YourStructType, h::UInt) = struct_hash(a, h) |
struct_equal |
Base.:(==)(a::YourStructType, b::YourStructType) = struct_equal(a, b) |
struct_isequal |
Base.isequal(a::YourStructType, b::YourStructType) = struct_isequal(a, b) |
struct_isapprox |
Base.isapprox(a::YourStructType, b::YourStructType; kwargs...) = struct_isapprox(a, b; kwargs...) |
Struct types have an ==
implementation by default which uses ===
, i.e. object identity, on the underlying components, in order to compare structs. (The same holds true for hash
, which should always follow the implementation of ==
)
Let's define a struct
struct MyStruct
a::Int
b::Vector
end
The default ==
fails to compare two structs with the same content
MyStruct(1, [2,3]) == MyStruct(1, [2,3]) # false
To fix this use the supplied macro @struct_hash_equal
@struct_hash_equal MyStruct
MyStruct(1, [2,3]) == MyStruct(1, [2,3]) # true
Alternatively you can use the macro right on struct definition
@struct_hash_equal struct MyStruct2
a::Int
b::Vector
end
MyStruct2(1, [2,3]) == MyStruct2(1, [2,3]) # true
You could also merely use @struct_equal
instead of @struct_hash_equal
, however it is recommended to always implement hash
and ==
together.
The implementation uses generated functions, which generate optimal code, specified to your custom struct type.
Inspecting the macro with
@macroexpand @struct_hash_equal MyStruct
returns the following
quote
Base.hash(a::MyStruct, h::UInt) = begin
StructEquality.struct_hash(a, h)
end
Base.:(==)(a::MyStruct, b::MyStruct) = begin
StructEquality.struct_equal(a, b)
end
end
In order to inspect generated functions, the @code_lowered
macro is best.
struct MyStruct
a::Int
b::Vector
end
@code_lowered struct_equal(MyStruct(1, [2,3]), MyStruct(1, [2,3]))
which returns
@ /path/to/StructEquality/src/StructEquality.jl:15 within `struct_equal`
┌ @ /path/to/StructEquality/src/StructEquality.jl within `macro expansion`
1 ─│ %1 = Base.getproperty(e1, :a)
│ │ %2 = Base.getproperty(e2, :a)
│ │ %3 = %1 == %2
└──│ goto #3 if not %3
2 ─│ %5 = Base.getproperty(e1, :b)
│ │ %6 = Base.getproperty(e2, :b)
│ │ %7 = %5 == %6
└──│ return %7
3 ─│ return false
└
)
It is like you would expect. the generated function extracts the field names and defines ==
by referring to ==
comparison of the fields.
For more details to this topic, please see this discourse thread https://discourse.julialang.org/t/surprising-struct-equality-test/4890/9 and this issue JuliaLang/julia#4648