|
1 |
| -[](https://travis-ci.org/andrewcooke/AutoHashEquals.jl) |
2 |
| -[](https://coveralls.io/r/andrewcooke/AutoHashEquals.jl) |
| 1 | +[](https://github.com/JuliaServices/AutoHashEquals.jl/actions/workflows/CI.yml?query=branch%3Amain) |
| 2 | +[](https://codecov.io/gh/JuliaServices/AutoHashEquals.jl) |
3 | 3 |
|
4 |
| -# AutoHashEquals |
| 4 | +# AutoHashEquals.jl - Automatically define hash and equals for Julia. |
5 | 5 |
|
6 |
| -A macro to add == and hash() to composite types (ie struct and mutable struct |
7 |
| -blocks). |
| 6 | +A macro to add `==` and `hash()` to struct types: `@auto_hash_equals`. |
8 | 7 |
|
9 |
| -For example: |
| 8 | +# `@auto_hash_equals` |
| 9 | + |
| 10 | +The macro `@auto_hash_equals` produces an implementation of `Base.hash(x)` that computes the hash code when invoked. |
| 11 | + |
| 12 | +You use it like so: |
10 | 13 |
|
11 | 14 | ```julia
|
12 |
| -@auto_hash_equals mutable struct Foo |
13 |
| - a::Int |
14 |
| - b |
| 15 | +@auto_hash_equals struct Box{T} |
| 16 | + x::T |
15 | 17 | end
|
16 | 18 | ```
|
17 | 19 |
|
18 |
| -becomes |
| 20 | +which is translated to |
19 | 21 |
|
20 | 22 | ```julia
|
21 |
| -mutable struct Foo |
22 |
| - a::Int |
23 |
| - b |
| 23 | +struct Box{T} |
| 24 | + x::T |
24 | 25 | end
|
25 |
| -Base.hash(a::Foo, h::UInt) = hash(a.b, hash(a.a, hash(:Foo, h))) |
26 |
| -Base.(:(==))(a::Foo, b::Foo) = isequal(a.b, b.b) && isequal(a.a, b.a) && true |
| 26 | +Base.hash(x::Box, h::UInt) = hash(x.x, hash(:Box, h)) |
| 27 | +Base.(:(==))(a::Box, b::Box) = isequal(a.x, b.x) |
27 | 28 | ```
|
28 | 29 |
|
29 |
| -Where |
| 30 | +We do not take the type arguments of a generic type into account for either `hash` or `==` unless `typearg=true` is specified (see below). So a `Box{Int}(1)` will test equal to a `Box{Any}(1)`. |
| 31 | + |
| 32 | +## User-specified hash function |
| 33 | + |
| 34 | +You can specify the hash function to be implemented, by naming it before the struct definition with a keyword argument `hashfn`: |
| 35 | + |
| 36 | +```julia |
| 37 | +@auto_hash_equals hashfn=SomePackage.myhash struct Foo |
| 38 | + x |
| 39 | + y |
| 40 | +end |
| 41 | +``` |
| 42 | + |
| 43 | +In this case the macro implements both `SomePackage.myhash` and `Base.hash` for `Foo`.` |
| 44 | + |
| 45 | +## Caching the hash value |
| 46 | + |
| 47 | +You can have the hash value precomputed and stored in a hidden field, by adding the keyword argument `cache=true`. This useful for non-mutable struct types that define recursive or deep data structures (and therefore are likely to be stored on the heap). It computes the hash code during construction and caches it in a field of the struct. If you are working with data structures of any significant depth, computing the hash once can speed things up at the expense of one additional field per struct. |
| 48 | + |
| 49 | +```julia |
| 50 | +@auto_hash_equals cache=true struct Box{T} |
| 51 | + x::T |
| 52 | +end |
| 53 | +``` |
| 54 | + |
| 55 | +this translates to |
| 56 | + |
| 57 | +```julia |
| 58 | +struct Box{T} |
| 59 | + x::T |
| 60 | + _cached_hash::UInt |
| 61 | + function Box{T}(x) where T |
| 62 | + new(x, Base.hash(x, Base.hash(:Box))) |
| 63 | + end |
| 64 | +end |
| 65 | +function Base.hash(x::Box, h::UInt) |
| 66 | + Base.hash(x._cached_hash, h) |
| 67 | +end |
| 68 | +function Base.hash(x::Box) |
| 69 | + x._cached_hash |
| 70 | +end |
| 71 | +function Base._show_default(io::IO, x::Box) |
| 72 | + AutoHashEqualsCached._show_default_auto_hash_equals_cached(io, x) |
| 73 | +end |
| 74 | +function Base.:(==)(a::Box, b::Box) |
| 75 | + a._cached_hash == b._cached_hash && Base.isequal(a.x, b.x) |
| 76 | +end |
| 77 | +function Box(x::T) where T |
| 78 | + Box{T}(x) |
| 79 | +end |
| 80 | +``` |
30 | 81 |
|
31 |
| -* we use `isequal()` because we want to match `Inf` values, etc. |
| 82 | +The definition of `_show_default(io,x)` prevents display of the `_cached_hash` field while preserving the behavior of `Base.show(...)` that handles self-recursive data structures without a stack overflow. |
32 | 83 |
|
33 |
| -* we include the type in the hash so that different types with the same |
34 |
| - contents don't collide |
| 84 | +We provide an external constructor for generic types so that you get the same type inference behavior you would get in the absence of this macro. Specifically, you can write `Box(1)` to get an object of type `Box{Int}`. |
35 | 85 |
|
36 |
| -* the type and `true` make it simple to generate code for empty records |
| 86 | +## Specifying significant fields |
37 | 87 |
|
38 |
| -* the `Base` module is explicitly used so that you don't need to |
39 |
| - import it |
| 88 | +You can specify which fields should be significant for the purposes of computing the hash function and checking equality: |
40 | 89 |
|
41 |
| -## Background |
| 90 | +```julia |
| 91 | +@auto_hash_equals fields=(a,b) struct Foo |
| 92 | + a |
| 93 | + b |
| 94 | + c |
| 95 | +end |
| 96 | +``` |
42 | 97 |
|
43 |
| -Julia has two composite types: *value* types, defined with `struct`, and |
44 |
| -*record* types, defined with `mutable struct`. |
| 98 | +this translates to |
45 | 99 |
|
46 |
| -Value types are intended for compact, immutable objects. They are stored on |
47 |
| -the stack, passed by value, and the default hash and equality are based on the |
48 |
| -literal bits in memory. |
| 100 | +```julia |
| 101 | +struct Foo |
| 102 | + a |
| 103 | + b |
| 104 | + c |
| 105 | +end |
| 106 | +function Base.hash(x::Foo, h::UInt) |
| 107 | + Base.hash(x.b, Base.hash(x.a, Base.hash(:Foo, h))) |
| 108 | +end |
| 109 | +function (Base).:(==)(a::Foo, b::Foo) |
| 110 | + Base.isequal(a.a, b.a) && Base.isequal(a.b, b.b) |
| 111 | +end |
| 112 | +``` |
49 | 113 |
|
50 |
| -Record types are allocated on the heap, are passed by reference, and the |
51 |
| -default hash and equality are based on the pointer value (the data address). |
| 114 | +## Specifying whether or not type arguments should be significant |
52 | 115 |
|
53 |
| -When you embed a record type in a value type, then the pointer to the record |
54 |
| -type becomes part of the value type, and so is included in equality and hash. |
| 116 | +You can specify that type arguments should be significant for the purposes of computing the hash function and checking equality by adding the keyword parameter `typearg=true`. By default they are not significant. You can specify the default (they are not significant) with `typearg=false`: |
55 | 117 |
|
56 |
| -Given the above, it is often necessary to define hash and equality for |
57 |
| -composite types. Particularly when record types are used (directly, or in a |
58 |
| -value type), and when records with the same contents are semantically equal. |
| 118 | +```julia-repl |
| 119 | +julia> @auto_hash_equals struct Box1{T} |
| 120 | + x::T |
| 121 | + end |
| 122 | +Box1 |
59 | 123 |
|
60 |
| -A common way to do this is to define the hash as a combination of the hashes |
61 |
| -of all the fields. Similarly, equality is often defined as equality of all |
62 |
| -fields. |
| 124 | +julia> Box1{Int}(1) == Box1{Any}(1) |
| 125 | +true |
63 | 126 |
|
64 |
| -This macro automates this common approach. |
| 127 | +julia> hash(Box1{Int}(1)) |
| 128 | +0x05014b35fc91d289 |
65 | 129 |
|
66 |
| -## Warnings |
| 130 | +julia> hash(Box1{Any}(1)) |
| 131 | +0x05014b35fc91d289 |
67 | 132 |
|
68 |
| -If you use this macro for a mutable type, then the hash depends on the |
69 |
| -contents of that type, so changing the contents changes the hash. Such types |
70 |
| -should not be stored in a hash table (Dict) and then mutated, because the |
71 |
| -objects will be "lost" (as the hash table *assumes* that hash is constant). |
| 133 | +julia> @auto_hash_equals typearg=true struct Box2{T} |
| 134 | + x::T |
| 135 | + end |
| 136 | +Box2 |
72 | 137 |
|
73 |
| -More generally, **this macro is only useful for mutable types when they are |
74 |
| -used as *immutable* records**. |
| 138 | +julia> Box2{Int}(1) == Box2{Any}(1) |
| 139 | +false |
75 | 140 |
|
76 |
| -## Credits |
| 141 | +julia> hash(Box2{Int}(1)) |
| 142 | +0xb7650cb555d6aafa |
77 | 143 |
|
78 |
| -Thanks to Michael Hatherly, Yichao Yu, and Carlo Lucibello. |
| 144 | +julia> hash(Box2{Any}(1)) |
| 145 | +0xefe691a94f296c61 |
| 146 | +``` |
0 commit comments