Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,21 @@ If `typearg=true`, then `e(t)` is used as the type seed, where `t` is the type o
Note that the value of `typeseed` is expected to be a `UInt` value when `typearg=false` (or `typearg` is not specified),
but a function that takes a type as its argument when `typearg=true`.

## Specifying that no equality or hash functions should be defined

You can specify that no `hash`, `==` or `isequal` is defined with `none=true`.
More precisely, this flag causes the functions to throw a `MethodError`

```
julia> @auto_hash_equals none=true struct Foo; x::Int; end

julia> hash(Foo(1))
ERROR: MethodError: no method matching hash(::Foo)

julia> isequal(Foo(1), Foo(2))
ERROR: MethodError: no method matching isequal(::Foo, ::Foo)
```

## Compatibility mode

In versions `v"1.0"` and earlier of `AutoHashEquals`, we produced a specialization of `Base.==`, implemented using `Base.isequal`.
Expand Down
1 change: 1 addition & 0 deletions src/AutoHashEquals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Options:
* `typearg=true|false` whether or not to make type arguments significant. Default: `false`.
* `typeseed=e` Use `e` (or `e(type)` if `typearg=true`) as the seed for hashing type arguments.
* `compat1=true` To have `==` defined by using `isequal`. Default: `false`.
* `none=true` To define `==`, `isequal` and `hash` to throw a `MethodError`. Default: `false`. (If true, the other keywords are ignored.)
"""
macro auto_hash_equals(args...)
kwargs = Dict{Symbol,Any}()
Expand Down
237 changes: 125 additions & 112 deletions src/impl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function auto_hash_equals_impl(__source__::LineNumberNode, typ; kwargs...)
typearg=false
typeseed=nothing
compat1=false
none=false

# Process the keyword arguments
for kw in kwargs
Expand Down Expand Up @@ -174,6 +175,8 @@ function auto_hash_equals_impl(__source__::LineNumberNode, typ; kwargs...)
fields = kw.second
elseif kw.first === :typeseed
typeseed = kw.second
elseif kw.first === :none
none = kw.second
elseif kw.first === :compat1
if !(kw.second isa Bool)
error_usage(__source__, "`compat1` argument must be a Bool, but got `$(kw.second)`.")
Expand All @@ -186,10 +189,10 @@ function auto_hash_equals_impl(__source__::LineNumberNode, typ; kwargs...)

typ = get_struct_decl(__source__::LineNumberNode, typ)

auto_hash_equals_impl(__source__, typ, fields, cache, hashfn, typearg, typeseed, compat1)
auto_hash_equals_impl(__source__, typ, fields, cache, hashfn, typearg, typeseed, none, compat1)
end

function auto_hash_equals_impl(__source__, struct_decl, fields, cache::Bool, hashfn, typearg::Bool, typeseed, compat1::Bool)
function auto_hash_equals_impl(__source__, struct_decl, fields, cache::Bool, hashfn, typearg::Bool, typeseed, none::Bool, compat1::Bool)
is_expr(struct_decl, :struct) || error_usage(__source__)

type_body = struct_decl.args[3].args
Expand Down Expand Up @@ -253,135 +256,145 @@ function auto_hash_equals_impl(__source__, struct_decl, fields, cache::Bool, has

result = Expr(:block, __source__, esc(struct_decl), __source__)

# add function for hash(x, h). hash(x)
if cache
push!(result.args, esc(:(function $hashfn(x::$type_name, h::UInt)
$hashfn(x._cached_hash, h)
end)))
push!(result.args, esc(:(function $hashfn(x::$type_name)
x._cached_hash
end)))
else
hash_init =
if isnothing(typeseed)
if typearg
:($type_seed($full_type_name, h))
if !none
# add function for hash(x, h). hash(x)
if cache
push!(result.args, esc(:(function $hashfn(x::$type_name, h::UInt)
$hashfn(x._cached_hash, h)
end)))
push!(result.args, esc(:(function $hashfn(x::$type_name)
x._cached_hash
end)))
else
hash_init =
if isnothing(typeseed)
if typearg
:($type_seed($full_type_name, h))
else
:($hashfn($(QuoteNode(type_name)), h))
end
else
:($hashfn($(QuoteNode(type_name)), h))
if typearg
:(UInt($typeseed($full_type_name, h)))
else
:(h + UInt($typeseed))
end
end
else
if typearg
:(UInt($typeseed($full_type_name, h)))
compute_hash = foldl(
(r, a) -> :($hashfn($getfield(x, $(QuoteNode(a))), $r)),
fields;
init = hash_init)
if typearg
if isnothing(where_list)
push!(result.args, esc(:(function $hashfn(x::$full_type_name, h::UInt)
$compute_hash
end)))
else
:(h + UInt($typeseed))
push!(result.args, esc(:(function $hashfn(x::$full_type_name, h::UInt) where {$(where_list...)}
$compute_hash
end)))
end
end
compute_hash = foldl(
(r, a) -> :($hashfn($getfield(x, $(QuoteNode(a))), $r)),
fields;
init = hash_init)
if typearg
if isnothing(where_list)
push!(result.args, esc(:(function $hashfn(x::$full_type_name, h::UInt)
$compute_hash
end)))
else
push!(result.args, esc(:(function $hashfn(x::$full_type_name, h::UInt) where {$(where_list...)}
$compute_hash
end)))
push!(result.args, esc(:(function $hashfn(x::$type_name, h::UInt)
$compute_hash
end)))
end
else
push!(result.args, esc(:(function $hashfn(x::$type_name, h::UInt)
$compute_hash
end)))
end
end

if hashfn != base_hash_name
# add function for Base.hash(x, h)
push!(result.args, esc(:(function $base_hash_name(x::$type_name, h::UInt)
$hashfn(x, h)
end)))
if cache
push!(result.args, esc(:(function $base_hash_name(x::$type_name)
$hashfn(x)
end)))
if hashfn != base_hash_name
# add function for Base.hash(x, h)
push!(result.args, esc(:(function $base_hash_name(x::$type_name, h::UInt)
$hashfn(x, h)
end)))
if cache
push!(result.args, esc(:(function $base_hash_name(x::$type_name)
$hashfn(x)
end)))
end
end
end

# add function Base.show
if cache
push!(result.args, esc(:(function $Base._show_default(io::IO, x::$type_name)
$_show_default_auto_hash_equals_cached(io, x)
end)))
end

# Add function to interoperate with Rematch2 (eventually Match.jl) if loaded
# at the time the macro is expanded.
if_has_package("Match", Base.UUID("7eb4fadd-790c-5f42-8a69-bfa0b872bfbf"), v"2") do pkg
if :match_fieldnames in names(pkg; all=true)
push!(result.args, esc(:(function $pkg.match_fieldnames(::Type{$type_name})
$((fields...,))
end)))
# add function Base.show
if cache
push!(result.args, esc(:(function $Base._show_default(io::IO, x::$type_name)
$_show_default_auto_hash_equals_cached(io, x)
end)))
end
end

if cache && !isnothing(where_list)
# for generic types, we add an external constructor to perform ctor type inference:
push!(result.args, esc(quote
$type_name($(member_decls...)) where {$(where_list...)} = $full_type_name($(member_names...))
end))
end
# Add function to interoperate with Rematch2 (eventually Match.jl) if loaded
# at the time the macro is expanded.
if_has_package("Match", Base.UUID("7eb4fadd-790c-5f42-8a69-bfa0b872bfbf"), v"2") do pkg
if :match_fieldnames in names(pkg; all=true)
push!(result.args, esc(:(function $pkg.match_fieldnames(::Type{$type_name})
$((fields...,))
end)))
end
end

# Add the `==` and `isequal` functions
for eq in (==, isequal)
# In compat mode, only define ==
eq == isequal && compat1 && continue
if cache && !isnothing(where_list)
# for generic types, we add an external constructor to perform ctor type inference:
push!(result.args, esc(quote
$type_name($(member_decls...)) where {$(where_list...)} = $full_type_name($(member_names...))
end))
end

if eq == isequal || compat1
equality_impl = foldl(
(r, f) -> :($r && $isequal($getfield(a, $(QuoteNode(f))), $getfield(b, $(QuoteNode(f))))),
fields;
init = cache ? :(a._cached_hash == b._cached_hash) : true)
if struct_decl.args[1]
# mutable structs can efficiently be compared by reference
# Note this optimization is only valid for `isequal`, e.g.
# a = [missing]
# a == a # missing
# isequal(a, a) # true
equality_impl = :(a === b || $equality_impl)
end
else
# Here we have a more complicated implementation in order to handle missings correctly.
# If any field comparison is false, we return false (even if some return missing).
# If no field comparisons are false, but one comparison missing, then we return missing.
# Otherwise we return true.
# (This matches the semantics of `==` for `Tuple`'s and `NamedTuple`'s.)
equality_impl = Expr(:block, :(found_missing = false))
if cache
push!(equality_impl.args, :(a._cached_hash != b._cached_hash && return false))
# Add the `==` and `isequal` functions
for eq in (==, isequal)
# In compat mode, only define ==
eq == isequal && compat1 && continue

if eq == isequal || compat1
equality_impl = foldl(
(r, f) -> :($r && $isequal($getfield(a, $(QuoteNode(f))), $getfield(b, $(QuoteNode(f))))),
fields;
init = cache ? :(a._cached_hash == b._cached_hash) : true)
if struct_decl.args[1]
# mutable structs can efficiently be compared by reference
# Note this optimization is only valid for `isequal`, e.g.
# a = [missing]
# a == a # missing
# isequal(a, a) # true
equality_impl = :(a === b || $equality_impl)
end
else
# Here we have a more complicated implementation in order to handle missings correctly.
# If any field comparison is false, we return false (even if some return missing).
# If no field comparisons are false, but one comparison missing, then we return missing.
# Otherwise we return true.
# (This matches the semantics of `==` for `Tuple`'s and `NamedTuple`'s.)
equality_impl = Expr(:block, :(found_missing = false))
if cache
push!(equality_impl.args, :(a._cached_hash != b._cached_hash && return false))
end
for f in fields
push!(equality_impl.args, :(cmp = $getfield(a, $(QuoteNode(f))) == $getfield(b, $(QuoteNode(f)))))
push!(equality_impl.args, :(cmp === false && return false))
push!(equality_impl.args, :($ismissing(cmp) && (found_missing = true)))
end
push!(equality_impl.args, :(return $ifelse(found_missing, missing, true)))
end
for f in fields
push!(equality_impl.args, :(cmp = $getfield(a, $(QuoteNode(f))) == $getfield(b, $(QuoteNode(f)))))
push!(equality_impl.args, :(cmp === false && return false))
push!(equality_impl.args, :($ismissing(cmp) && (found_missing = true)))

fn_name = Symbol(eq)
if isnothing(where_list) || !typearg
push!(result.args, esc(:(function ($Base).$fn_name(a::$type_name, b::$type_name)
$equality_impl
end)))
else
# If requested, require the type arguments be the same for two instances to be equal
push!(result.args, esc(:(function ($Base).$fn_name(a::$full_type_name, b::$full_type_name) where {$(where_list...)}
$equality_impl
end)))
end
push!(equality_impl.args, :(return $ifelse(found_missing, missing, true)))
end

fn_name = Symbol(eq)
if isnothing(where_list) || !typearg
push!(result.args, esc(:(function ($Base).$fn_name(a::$type_name, b::$type_name)
$equality_impl
end)))
else
# If requested, require the type arguments be the same for two instances to be equal
push!(result.args, esc(:(function ($Base).$fn_name(a::$full_type_name, b::$full_type_name) where {$(where_list...)}
$equality_impl
end)))
else
# none
push!(result.args, esc(:($base_hash_name(x::$type_name, h::UInt) = throw(MethodError($base_hash_name, (x, h))))))
push!(result.args, esc(:($base_hash_name(x::$type_name) = throw(MethodError($base_hash_name, (x,))))))
eq_name = :($Base.eq)
for eq in (==, isequal)
eqname = :($Base.$(Symbol(eq)))
push!(result.args, esc(:($eqname(a::$type_name, b::$type_name) = throw(MethodError($eqname, (a, b))))))
end
end

# Evaluating a struct declaration normally returns the struct itself.
# we preserve that behavior when the macro is used.
# We also relay documentation to the struct type.
Expand Down
8 changes: 8 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,14 @@ abstract type B{T} end
end

end
@testset "test option none=true" begin
@auto_hash_equals none=true struct Non; x::Int; end
non = Non(1)
@test_throws MethodError non == non
@test_throws MethodError isequal(non, non)
@test_throws MethodError hash(non)
@test_throws MethodError hash(non, 1)
end
end

end # module