Skip to content

Commit

Permalink
Update the contents of this entire package (#32)
Browse files Browse the repository at this point in the history
from what used to be AutoHashEqualsCached.jl
Also correct the uuid - it has been wrong for two years!
  • Loading branch information
Neal Gafter authored Aug 18, 2023
1 parent f643bdd commit 0ebd13e
Show file tree
Hide file tree
Showing 15 changed files with 1,218 additions and 252 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI
on:
push:
branches:
- main
tags: ['*']
pull_request:
concurrency:
# Skip intermediate builds: always.
# Cancel intermediate builds: only if it is a pull request build.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- '1.6'
- '1.7'
- '1.8'
- '1.9'
- 'nightly'
os:
- ubuntu-latest
arch:
- x64
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v1
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v2
with:
files: lcov.info
16 changes: 16 additions & 0 deletions .github/workflows/CompatHelper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: CompatHelper
on:
schedule:
- cron: 0 0 * * *
workflow_dispatch:
jobs:
CompatHelper:
runs-on: ubuntu-latest
steps:
- name: Pkg.add("CompatHelper")
run: julia -e 'using Pkg; Pkg.add("CompatHelper")'
- name: CompatHelper.main()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }}
run: julia -e 'using CompatHelper; CompatHelper.main()'
9 changes: 6 additions & 3 deletions .github/workflows/TagBot.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
name: TagBot
on:
schedule:
- cron: 0 * * * *
issue_comment:
types:
- created
workflow_dispatch:
jobs:
TagBot:
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
runs-on: ubuntu-latest
steps:
- uses: JuliaRegistries/TagBot@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

ssh: ${{ secrets.DOCUMENTER_KEY }}
16 changes: 16 additions & 0 deletions .github/workflows/register.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Register Package
on:
workflow_dispatch:
inputs:
version:
description: Version to register or component to bump
required: true
jobs:
register:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: julia-actions/RegisterAction@latest
with:
token: ${{ secrets.GITHUB_TOKEN }}
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
*.jl.*.cov
*.jl.cov
*.jl.mem

/Manifest.toml
Manifest.toml
/docs/build/
/tmp/*
.vscode
16 changes: 0 additions & 16 deletions .travis.yml

This file was deleted.

41 changes: 21 additions & 20 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# AutoHashEquals.jl - Automatically define hash and equals for Julia.

The AutoHashEquals.jl package is licensed under the MIT "Expat" License:

> Copyright (c) 2015: andrew cooke.
>
> Permission is hereby granted, free of charge, to any person obtaining
> a copy of this software and associated documentation files (the
> "Software"), to deal in the Software without restriction, including
> without limitation the rights to use, copy, modify, merge, publish,
> distribute, sublicense, and/or sell copies of the Software, and to
> permit persons to whom the Software is furnished to do so, subject to
> the following conditions:
>
> The above copyright notice and this permission notice shall be
> included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright (c) 2015-2023: andrew cooke, RelationalAI, Inc, and contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
17 changes: 6 additions & 11 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
name = "AutoHashEquals"
uuid = "97026771-8bcd-44fb-b7eb-f644370d63d3"
authors = ["andrew cooke <[email protected]>"]
uuid = "15f4f7f2-30c1-5605-9d31-71845cf9641f"
authors = ["Neal Gafter <[email protected]>", "andrew cooke <[email protected]>"]
version = "1.0.0"

[compat]
julia = "1"

[extras]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[deps]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[targets]
test = ["Markdown", "Serialization", "Test"]
[compat]
julia = "1.6"
164 changes: 116 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,146 @@
[![Build Status](https://travis-ci.org/andrewcooke/AutoHashEquals.jl.png)](https://travis-ci.org/andrewcooke/AutoHashEquals.jl)
[![Coverage Status](https://coveralls.io/repos/andrewcooke/AutoHashEquals.jl/badge.svg)](https://coveralls.io/r/andrewcooke/AutoHashEquals.jl)
[![Build Status](https://github.com/JuliaServices/AutoHashEquals.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaServices/AutoHashEquals.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/JuliaServices/AutoHashEquals.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaServices/AutoHashEquals.jl)

# AutoHashEquals
# AutoHashEquals.jl - Automatically define hash and equals for Julia.

A macro to add == and hash() to composite types (ie struct and mutable struct
blocks).
A macro to add `==` and `hash()` to struct types: `@auto_hash_equals`.

For example:
# `@auto_hash_equals`

The macro `@auto_hash_equals` produces an implementation of `Base.hash(x)` that computes the hash code when invoked.

You use it like so:

```julia
@auto_hash_equals mutable struct Foo
a::Int
b
@auto_hash_equals struct Box{T}
x::T
end
```

becomes
which is translated to

```julia
mutable struct Foo
a::Int
b
struct Box{T}
x::T
end
Base.hash(a::Foo, h::UInt) = hash(a.b, hash(a.a, hash(:Foo, h)))
Base.(:(==))(a::Foo, b::Foo) = isequal(a.b, b.b) && isequal(a.a, b.a) && true
Base.hash(x::Box, h::UInt) = hash(x.x, hash(:Box, h))
Base.(:(==))(a::Box, b::Box) = isequal(a.x, b.x)
```

Where
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)`.

## User-specified hash function

You can specify the hash function to be implemented, by naming it before the struct definition with a keyword argument `hashfn`:

```julia
@auto_hash_equals hashfn=SomePackage.myhash struct Foo
x
y
end
```

In this case the macro implements both `SomePackage.myhash` and `Base.hash` for `Foo`.`

## Caching the hash value

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.

```julia
@auto_hash_equals cache=true struct Box{T}
x::T
end
```

this translates to

```julia
struct Box{T}
x::T
_cached_hash::UInt
function Box{T}(x) where T
new(x, Base.hash(x, Base.hash(:Box)))
end
end
function Base.hash(x::Box, h::UInt)
Base.hash(x._cached_hash, h)
end
function Base.hash(x::Box)
x._cached_hash
end
function Base._show_default(io::IO, x::Box)
AutoHashEqualsCached._show_default_auto_hash_equals_cached(io, x)
end
function Base.:(==)(a::Box, b::Box)
a._cached_hash == b._cached_hash && Base.isequal(a.x, b.x)
end
function Box(x::T) where T
Box{T}(x)
end
```

* we use `isequal()` because we want to match `Inf` values, etc.
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.

* we include the type in the hash so that different types with the same
contents don't collide
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}`.

* the type and `true` make it simple to generate code for empty records
## Specifying significant fields

* the `Base` module is explicitly used so that you don't need to
import it
You can specify which fields should be significant for the purposes of computing the hash function and checking equality:

## Background
```julia
@auto_hash_equals fields=(a,b) struct Foo
a
b
c
end
```

Julia has two composite types: *value* types, defined with `struct`, and
*record* types, defined with `mutable struct`.
this translates to

Value types are intended for compact, immutable objects. They are stored on
the stack, passed by value, and the default hash and equality are based on the
literal bits in memory.
```julia
struct Foo
a
b
c
end
function Base.hash(x::Foo, h::UInt)
Base.hash(x.b, Base.hash(x.a, Base.hash(:Foo, h)))
end
function (Base).:(==)(a::Foo, b::Foo)
Base.isequal(a.a, b.a) && Base.isequal(a.b, b.b)
end
```

Record types are allocated on the heap, are passed by reference, and the
default hash and equality are based on the pointer value (the data address).
## Specifying whether or not type arguments should be significant

When you embed a record type in a value type, then the pointer to the record
type becomes part of the value type, and so is included in equality and hash.
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`:

Given the above, it is often necessary to define hash and equality for
composite types. Particularly when record types are used (directly, or in a
value type), and when records with the same contents are semantically equal.
```julia-repl
julia> @auto_hash_equals struct Box1{T}
x::T
end
Box1
A common way to do this is to define the hash as a combination of the hashes
of all the fields. Similarly, equality is often defined as equality of all
fields.
julia> Box1{Int}(1) == Box1{Any}(1)
true
This macro automates this common approach.
julia> hash(Box1{Int}(1))
0x05014b35fc91d289
## Warnings
julia> hash(Box1{Any}(1))
0x05014b35fc91d289
If you use this macro for a mutable type, then the hash depends on the
contents of that type, so changing the contents changes the hash. Such types
should not be stored in a hash table (Dict) and then mutated, because the
objects will be "lost" (as the hash table *assumes* that hash is constant).
julia> @auto_hash_equals typearg=true struct Box2{T}
x::T
end
Box2
More generally, **this macro is only useful for mutable types when they are
used as *immutable* records**.
julia> Box2{Int}(1) == Box2{Any}(1)
false
## Credits
julia> hash(Box2{Int}(1))
0xb7650cb555d6aafa
Thanks to Michael Hatherly, Yichao Yu, and Carlo Lucibello.
julia> hash(Box2{Any}(1))
0xefe691a94f296c61
```
1 change: 0 additions & 1 deletion REQUIRE

This file was deleted.

Loading

0 comments on commit 0ebd13e

Please sign in to comment.