10 Commits

Author SHA1 Message Date
46b59d743e feat: custom lint rule, fixed readme 2026-04-29 20:54:05 -04:00
39548b4332 feat!: Drop returns bool, Put doesn't stack-overflow (#21)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 58s
CI / Makefile Lint (push) Successful in 55s
CI / Markdown Lint (push) Successful in 34s
CI / Unit Tests (push) Successful in 54s
CI / Fuzz Tests (push) Successful in 1m26s
CI / Mutation Tests (push) Successful in 1m11s
## Description

Closes #11.

## Changes

### Design Decisions

## Checklist

- [ ] Tests pass
- [ ] Docs updated

Reviewed-on: #21
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-17 01:31:01 +00:00
29ba6bfd4d fix!: no mixed receiver types (#23)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Makefile Lint (push) Successful in 50s
CI / Go Lint (push) Successful in 54s
CI / Markdown Lint (push) Successful in 50s
CI / Unit Tests (push) Successful in 47s
CI / Mutation Tests (push) Successful in 1m31s
CI / Fuzz Tests (push) Successful in 1m21s
## Description

Currently, `bucket` and `Table` have mixed receiver types: some are pointer receviers, and others are value receivers.

As per the Go Wiki, [you can have value and pointer receivers, just don't mix them](https://go.dev/doc/faq#methods_on_values_or_pointers).

## Changes

- Replace all value receivers in `bucket` and `Table` with pointer receivers.

### Design Decisions

This decision was made due to the advice on the Go wiki.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #23
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-16 03:27:48 +00:00
7cc1657403 refactor!: shorter constructors, bucketsubtable (#22)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Makefile Lint (push) Successful in 47s
CI / Go Lint (push) Successful in 51s
CI / Markdown Lint (push) Successful in 46s
CI / Unit Tests (push) Successful in 47s
CI / Fuzz Tests (push) Successful in 1m19s
CI / Mutation Tests (push) Successful in 1m36s
## Description

Currently, the name of `bucket` is a bit confusing, because it is considered a 'table' in literature (as well as the whole hash table). A `bucket` is better described as a 'subtable', which is used by the total hash table to perform cuckoo hashing.

In addition, the constructors `NewTable`, `NewTableBy`, and `NewCustomTable` were given shorter names, because the package name `cuckoo` already implies that `New*` would create a hash table with cuckoo hashing. This package has one use-case, and so it unambiguous what constructors produce.

## Changes

- `NewTable` -> `New`
- `NewTableBy` -> `NewBy`
- `NewCustomTable` -> `NewCustom`
- `bucket` -> `subtable`

### Design Decisions

- I would have renamed `Table` and `subtable` to map equivalents, but 'submap' implies that a certain subsection of the map is contained within it, which isn't quite right.
- I chose not to go with `Map` and `table`, because of the split naming convention.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #22
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-16 03:15:39 +00:00
42c5b5f8f4 feat!: update get from (V, error) to (V, bool) (#20)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 43s
CI / Makefile Lint (push) Successful in 41s
CI / Markdown Lint (push) Successful in 32s
CI / Unit Tests (push) Successful in 39s
CI / Fuzz Tests (push) Successful in 1m44s
CI / Mutation Tests (push) Successful in 1m28s
## Description

Currently, the signature for `Table.Get` is `func (K) (V, error)`. This is not very Go-idiomatic, which prefers to return a boolean instead of an error. For instance, a built-in Go map is used like so:

```go
if value, ok := users[id]; !ok {
  // ...
}
```

Updating our table to look like that is best practice. In that same vein, to support direct lookup (i.e. `v := users[id]`), this PR also adds `Table.Find`.

## Changes

- BREAKING CHANGE: Update contract of `Table.Get` to `func (K) (V, bool)`. Returns 'false' is the item cannot be found, and 'true' if it is found.
- Add `Table.Find`.
- Updated tests and documentation to match the change.

### Design Decisions

- Chose to make this decision because throwing an error implies that there is something 'wrong' with the table. There is nothing wrong with the table; it is just that the item does not exist.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #20
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-14 01:58:15 +00:00
867a1d49df feat: sentinel error ErrBadHash (#19)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Makefile Lint (push) Successful in 1m4s
CI / Markdown Lint (push) Successful in 32s
CI / Go Lint (push) Successful in 1m15s
CI / Unit Tests (push) Successful in 38s
CI / Fuzz Tests (push) Successful in 1m34s
CI / Mutation Tests (push) Successful in 1m31s
## Description

Currently, the errors are not sentinel, and so are hard to test for. This PR makes sure hash collision errors are accounted for.

## Changes

- Add `ErrBadHash`. Happens when there are too many collisions for an item to be added.

### Design Decisions

- Chose to name `ErrBadHash` over `ErrCycle` because the feedbach that the user should be given is to evaluate their hash functions. Cycle collision is a bit esoteric.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #19
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-14 00:38:11 +00:00
322d71f0be refactor!: remove MinimumLoad() option (#17)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 38s
CI / Makefile Lint (push) Successful in 36s
CI / Markdown Lint (push) Successful in 21s
CI / Unit Tests (push) Successful in 37s
CI / Fuzz Tests (push) Successful in 1m9s
CI / Mutation Tests (push) Successful in 1m18s
## Description

The `cuckoo.MinimumLoad()` option was not a very useful option, and prone to error. By removing the ability to modify it, and setting it to something reasonable (like 5%), we can remove a whole set of errors that the user may stumble into.

## Changes

- Remove `MinimumLoad()` option.
- Privated `DefaultMinimumLoad`.

### Design Decisions

- `DefaultMinimumLoad` should be privated because it is no longer an option. The user should not need to interact with it.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #17
2026-04-03 14:51:41 +00:00
ed30a4fc7c fix: check-pr-title job has prompt injection (#18)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 36s
CI / Makefile Lint (push) Successful in 35s
CI / Markdown Lint (push) Successful in 22s
CI / Unit Tests (push) Successful in 35s
CI / Fuzz Tests (push) Successful in 1m6s
CI / Mutation Tests (push) Successful in 1m10s
## Description

Currently, the `check-pr-title` job has a security vulnerability. If you give the PR a bad title, the job can run arbitrary code.

## Changes

- Fix prompt injection by pulling the PR title as an environment variable.
- Also, restricted the job to only `pull_request` trigger.

### Design Decisions

- It is better to pull out this job into a separate workflow with a unique trigger, but I chose not to because it is currently only one job.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #18
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 14:47:01 +00:00
c834f004a2 chore: no free form issues, 'description'-s to 'placeholders'-s (#16)
Some checks failed
CI / Check PR Title (push) Failing after 19s
CI / Makefile Lint (push) Successful in 34s
CI / Go Lint (push) Successful in 37s
CI / Markdown Lint (push) Successful in 35s
CI / Unit Tests (push) Successful in 33s
CI / Fuzz Tests (push) Successful in 1m4s
CI / Mutation Tests (push) Successful in 2m55s
## Description

There were some problems with the roll-out of the issue templates. This PR addresses them.

## Changes

- Disable `blank-issue-enabled`.
- Move all `description` types in the issue templates to `placeholder`.

### Design Decisions

- The `description` fields take up too much space.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #16
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 13:57:03 +00:00
581f26f562 ci: support semantic versioning, conventional commits (#15)
Some checks failed
CI / Check PR Title (push) Failing after 18s
CI / Makefile Lint (push) Successful in 33s
CI / Markdown Lint (push) Successful in 22s
CI / Unit Tests (push) Successful in 30s
CI / Go Lint (push) Successful in 45s
CI / Mutation Tests (push) Successful in 1m18s
CI / Fuzz Tests (push) Successful in 1m48s
## Description

Currently, the repository is not well suited for semantic versioning, and hasn't much to support it. This PR adds templates, CI jobs, and configs to simplify its adoption.

## Changes

- Added `FEATURE` and `BUG` issue templates. Also, forbids free-form issues.
- Adds a PR template.
- Adds a CI job to ensure the commit title follows conventional commits.

### Design Decisions

N/A.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #15
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 13:46:05 +00:00
21 changed files with 402 additions and 306 deletions

View File

@@ -7,20 +7,20 @@ body:
id: context id: context
attributes: attributes:
label: Context label: Context
description: What circumstances led to the bug? placeholder: What circumstances led to the bug?
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: expected-behavior id: expected-behavior
attributes: attributes:
label: Expected Behavior label: Expected Behavior
description: What did you expect would happen? placeholder: What did you expect would happen?
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: actual-behavior id: actual-behavior
attributes: attributes:
label: Actual Behavior label: Actual Behavior
description: What happened, and why was it unexpected? placeholder: What happened, and why was it unexpected?
validations: validations:
required: true required: true

View File

@@ -6,18 +6,10 @@ name: ✨ Feature Request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "[FEATURE]: " title: "[FEATURE]: "
body: body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! 🤗
Please make sure this feature request hasn't been already submitted by
someone by looking through other open/closed issues. 😃
- type: dropdown - type: dropdown
attributes: attributes:
multiple: false multiple: false
label: Type of Feature label: Feature Type
description: Select the type of feature request.
options: options:
- "✨ New Feature" - "✨ New Feature"
- "📝 Documentation" - "📝 Documentation"
@@ -31,7 +23,7 @@ body:
id: description id: description
attributes: attributes:
label: Description label: Description
description: | placeholder: |
Give us a brief description of the feature or enhancement you would Give us a brief description of the feature or enhancement you would
like! like!
validations: validations:
@@ -40,6 +32,6 @@ body:
id: additional-information id: additional-information
attributes: attributes:
label: Additional Information label: Additional Information
description: | placeholder: |
Give us some additional information on the feature request like proposed Give us some additional information on the feature request like proposed
solutions, links, screenshots, etc. solutions, links, screenshots, etc.

View File

@@ -1,2 +1,2 @@
# yaml-language-server: $schema=https://www.schemastore.org/gitea-issue-config.json # yaml-language-server: $schema=https://www.schemastore.org/gitea-issue-config.json
blank_issues_enabled: true blank_issues_enabled: false

View File

@@ -0,0 +1,17 @@
---
name: "New Pull Request"
about: "Standard PR template"
title: ""
ref: "main"
---
## Description
## Changes
### Design Decisions
## Checklist
- [ ] Tests pass
- [ ] Docs updated

View File

@@ -9,10 +9,12 @@ jobs:
check-pr-title: check-pr-title:
name: Check PR Title name: Check PR Title
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
env:
TITLE: ${{ gitea.event.pull_request.title }}
steps: steps:
- run: | - run: |
TITLE="${{ gitea.event.pull_request.title }}" if ! echo "$TITLE" | grep -qE '^(WIP: )?(feat|fix|docs|chore|ci|test|refactor|perf|build|style|revert)(\(.+\))?(!)?: .+'; then
if ! echo "$TITLE" | grep -qE '^(feat|fix|docs|chore|ci|test|refactor|perf|build|style|revert)(\(.+\))?(!)?: .+'; then
echo "::error::Pull Request title must follow conventional commits" echo "::error::Pull Request title must follow conventional commits"
exit 1 exit 1
fi fi

View File

@@ -114,6 +114,9 @@ linters:
# Reports uses of functions with replacement inside the testing package. # Reports uses of functions with replacement inside the testing package.
- usetesting - usetesting
# Reports mixed receiver types in structs/interfaces.
- recvcheck
settings: settings:
revive: revive:
rules: rules:

14
.markdownlint-cli2.jsonc Normal file
View File

@@ -0,0 +1,14 @@
{
"customRules": [".markdownlint-rules/one-sentence-per-line.mjs"],
"config": {
"default": true,
"heading-style": { "style": "atx" },
"ul-indent": { "indent": 2 },
"line-length": false,
"no-duplicate-heading": { "siblings_only": true },
"no-inline-html": {
"allowed_elements": ["br", "code", "details", "summary", "img", "picture", "source"]
},
"first-line-heading": true
}
}

View File

@@ -0,0 +1,48 @@
// @ts-check
/**
* @typedef {object} ErrorInfo
* @property {number} lineNumber
* @property {string} [context]
* @property {string} [detail]
* @property {[number, number]} [range]
*/
/**
* @typedef {object} Params
* @property {string[]} lines
*/
/** @typedef {(error: ErrorInfo) => void} OnError */
/**
* @typedef {object} Rule
* @property {string[]} names
* @property {string} description
* @property {string[]} tags
* @property {(params: Params, onError: OnError) => void} function
*/
/** @type {Rule} */
export default {
names: ["one-sentence-per-line"],
description: "Each sentence must be on its own line",
tags: ["sentences"],
function: function (params, onError) {
let inFence = false;
params.lines.forEach((line, index) => {
if (/^```/.test(line)) {
inFence = !inFence;
return;
}
if (inFence) return;
// Skip headings, blank lines, HTML, table rows
if (/^(#|\s*[|<]|>|\s*$)/.test(line)) return;
// Strip list marker before checking
const text = line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, "");
if (/[.!?]\s+[A-Z]/.test(text)) {
onError({ lineNumber: index + 1, context: text.trim() });
}
});
},
};

View File

@@ -1,17 +0,0 @@
default: true
heading-style:
style: atx
ul-indent:
indent: 2
line-length: false
no-duplicate-heading:
siblings_only: true
no-inline-html:
allowed_elements:
- br
- details
- summary
- img
- picture
- source
first-line-heading: true

View File

@@ -1,3 +1,6 @@
# <img height="30" src="assets/logo.svg" alt="Go Cuckoo, by `mvhutz`."> Go Cuckoo # <img height="30" src="assets/logo.svg" alt="Go Cuckoo, by `mvhutz`."> Go Cuckoo
A hash table that uses cuckoo hashing to achieve a worst-case O(1) lookup time. Read more about it in [the package documentation](https://pkg.go.dev/git.maximhutz.com/tools/go-cuckoo). A hash table that uses cuckoo hashing to achieve a worst-case O(1) lookup time.
Read more about it in [the package documentation][docs].
[docs]: https://pkg.go.dev/git.maximhutz.com/tools/go-cuckoo

103
bucket.go
View File

@@ -1,103 +0,0 @@
package cuckoo
type entry[K, V any] struct {
key K
value V
}
type slot[K, V any] struct {
entry[K, V]
occupied bool
}
type bucket[K, V any] struct {
hash Hash[K]
slots []slot[K, V]
capacity, size uint64
compare EqualFunc[K]
}
// location determines where in the bucket a certain key would be placed. If the
// capacity is 0, this will panic.
func (b bucket[K, V]) location(key K) uint64 {
return b.hash(key) % b.capacity
}
func (b bucket[K, V]) get(key K) (value V, found bool) {
if b.capacity == 0 {
return
}
slot := b.slots[b.location(key)]
return slot.value, slot.occupied && b.compare(slot.key, key)
}
func (b *bucket[K, V]) drop(key K) (occupied bool) {
if b.capacity == 0 {
return
}
slot := &b.slots[b.location(key)]
if slot.occupied && b.compare(slot.key, key) {
slot.occupied = false
b.size--
return true
}
return false
}
func (b *bucket[K, V]) resize(capacity uint64) {
b.slots = make([]slot[K, V], capacity)
b.capacity = capacity
b.size = 0
}
func (b bucket[K, V]) update(key K, value V) (updated bool) {
if b.capacity == 0 {
return
}
slot := &b.slots[b.location(key)]
if slot.occupied && b.compare(slot.key, key) {
slot.value = value
return true
}
return false
}
func (b *bucket[K, V]) evict(insertion entry[K, V]) (evicted entry[K, V], eviction bool) {
if b.capacity == 0 {
return insertion, true
}
slot := &b.slots[b.location(insertion.key)]
if !slot.occupied {
slot.entry = insertion
slot.occupied = true
b.size++
return
}
if b.compare(slot.key, insertion.key) {
slot.value = insertion.value
return
}
insertion, slot.entry = slot.entry, insertion
return insertion, true
}
func newBucket[K, V any](capacity uint64, hash Hash[K], compare EqualFunc[K]) bucket[K, V] {
return bucket[K, V]{
hash: hash,
capacity: capacity,
compare: compare,
size: 0,
slots: make([]slot[K, V], capacity),
}
}

View File

@@ -2,7 +2,7 @@ package cuckoo
// An EqualFunc determines whethers two keys are 'equal'. Keys that are 'equal' // An EqualFunc determines whethers two keys are 'equal'. Keys that are 'equal'
// are teated as the same by the [Table]. A good EqualFunc is pure, // are teated as the same by the [Table]. A good EqualFunc is pure,
// deterministic, and fast. By default, [NewTable] uses [DefaultEqualFunc]. // deterministic, and fast. By default, [New] uses [DefaultEqualFunc].
// //
// This function MUST NOT return true if the [Hash] digest of two keys // This function MUST NOT return true if the [Hash] digest of two keys
// are different: the [Table] will not work. // are different: the [Table] will not work.

View File

@@ -28,7 +28,7 @@ func ExampleEqualFunc_badEqualFunc() {
// Two users with the same ID are equal. // Two users with the same ID are equal.
isEqual := func(a, b User) bool { return a.ID == b.ID } isEqual := func(a, b User) bool { return a.ID == b.ID }
userbase := cuckoo.NewCustomTable[User, bool](makeHash(1), makeHash(2), isEqual) userbase := cuckoo.NewCustom[User, bool](makeHash(1), makeHash(2), isEqual)
(userbase.Put(User{"1", "Robert Doe"}, true)) (userbase.Put(User{"1", "Robert Doe"}, true))

View File

@@ -3,7 +3,6 @@ package cuckoo_test
import ( import (
"fmt" "fmt"
"maps" "maps"
"math"
"os" "os"
"testing" "testing"
@@ -30,7 +29,6 @@ type fuzzStep struct {
type fuzzScenario struct { type fuzzScenario struct {
seedA, seedB uint32 seedA, seedB uint32
capacity, growthFactor uint8 capacity, growthFactor uint8
load float64
steps []fuzzStep steps []fuzzStep
} }
@@ -48,7 +46,6 @@ func FuzzInsertLookup(f *testing.F) {
seedA, seedB := scenario.seedA, scenario.seedB seedA, seedB := scenario.seedA, scenario.seedB
growthFactor := max(2, int(scenario.growthFactor)) growthFactor := max(2, int(scenario.growthFactor))
capacity := int(scenario.capacity) capacity := int(scenario.capacity)
minimumLoad := math.Abs(math.Mod(scenario.load, 1.0))
// If they are the same number, the hashes will clash, always causing an // If they are the same number, the hashes will clash, always causing an
// error. // error.
@@ -56,43 +53,37 @@ func FuzzInsertLookup(f *testing.F) {
t.Skip() t.Skip()
} }
// If the load is too high, the hashs will not be able to allocate fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d\n",
// properly. seedA, seedB, capacity, growthFactor)
if minimumLoad > 0.20 {
t.Skip()
}
fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d minimumLoad=%f\n", actual := cuckoo.NewCustom[uint32, uint32](
seedA, seedB, capacity, growthFactor, minimumLoad)
actual := cuckoo.NewCustomTable[uint32, uint32](
offsetHash(seedA), offsetHash(seedA),
offsetHash(seedB), offsetHash(seedB),
func(a, b uint32) bool { return a == b }, func(a, b uint32) bool { return a == b },
cuckoo.Capacity(capacity), cuckoo.Capacity(capacity),
cuckoo.GrowthFactor(growthFactor), cuckoo.GrowthFactor(growthFactor),
cuckoo.MinimumLoad(minimumLoad),
) )
expected := map[uint32]uint32{} expected := map[uint32]uint32{}
for _, step := range scenario.steps { for _, step := range scenario.steps {
if step.drop { if step.drop {
err := actual.Drop(step.key) ok := actual.Drop(step.key)
assert.NoError(err) _, has := expected[step.key]
assert.Equal(ok, has)
delete(expected, step.key) delete(expected, step.key)
_, err = actual.Get(step.key) _, ok = actual.Get(step.key)
assert.Error(err) assert.False(ok)
} else { } else {
err := actual.Put(step.key, step.value) err := actual.Put(step.key, step.value)
assert.NoError(err) assert.NoError(err)
expected[step.key] = step.value expected[step.key] = step.value
found, err := actual.Get(step.key) found, ok := actual.Get(step.key)
assert.NoError(err) assert.True(ok)
assert.Equal(step.value, found) assert.Equal(step.value, found)
} }

View File

@@ -11,7 +11,7 @@ func TestMaxEvictions(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
for i := 16; i < 116; i++ { for i := 16; i < 116; i++ {
table := NewTable[int, bool](Capacity(i / 2)) table := New[int, bool](Capacity(i / 2))
expectedEvictions := 3 * math.Floor(math.Log2(float64(i))) expectedEvictions := 3 * math.Floor(math.Log2(float64(i)))
assert.Equal(table.maxEvictions(), int(expectedEvictions)) assert.Equal(table.maxEvictions(), int(expectedEvictions))
@@ -20,7 +20,7 @@ func TestMaxEvictions(t *testing.T) {
func TestLoad(t *testing.T) { func TestLoad(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := NewTable[int, bool](Capacity(8)) table := New[int, bool](Capacity(8))
for i := range 16 { for i := range 16 {
err := table.Put(i, true) err := table.Put(i, true)

View File

@@ -14,7 +14,7 @@ import (
func TestNewTable(t *testing.T) { func TestNewTable(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTable[int, bool]() table := cuckoo.New[int, bool]()
assert.NotNil(table) assert.NotNil(table)
assert.Zero(table.Size()) assert.Zero(table.Size())
@@ -23,7 +23,7 @@ func TestNewTable(t *testing.T) {
func TestAddItem(t *testing.T) { func TestAddItem(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key, value := 0, true key, value := 0, true
table := cuckoo.NewTable[int, bool]() table := cuckoo.New[int, bool]()
err := table.Put(key, value) err := table.Put(key, value)
@@ -35,7 +35,7 @@ func TestAddItem(t *testing.T) {
func TestPutOverwrite(t *testing.T) { func TestPutOverwrite(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key, value, newValue := 0, 1, 2 key, value, newValue := 0, 1, 2
table := cuckoo.NewTable[int, int]() table := cuckoo.New[int, int]()
(table.Put(key, value)) (table.Put(key, value))
err := table.Put(key, newValue) err := table.Put(key, newValue)
@@ -50,7 +50,7 @@ func TestPutOverwrite(t *testing.T) {
func TestSameHash(t *testing.T) { func TestSameHash(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
hash := func(int) uint64 { return 0 } hash := func(int) uint64 { return 0 }
table := cuckoo.NewCustomTable[int, bool](hash, hash, cuckoo.DefaultEqualFunc[int]) table := cuckoo.NewCustom[int, bool](hash, hash, cuckoo.DefaultEqualFunc[int])
errA := table.Put(0, true) errA := table.Put(0, true)
errB := table.Put(1, true) errB := table.Put(1, true)
@@ -63,14 +63,14 @@ func TestSameHash(t *testing.T) {
func TestStartingCapacity(t *testing.T) { func TestStartingCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTable[int, bool](cuckoo.Capacity(64)) table := cuckoo.New[int, bool](cuckoo.Capacity(64))
assert.Equal(uint64(128), table.TotalCapacity()) assert.Equal(uint64(128), table.TotalCapacity())
} }
func TestResizeCapacity(t *testing.T) { func TestResizeCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTable[int, bool]( table := cuckoo.New[int, bool](
cuckoo.Capacity(8), cuckoo.Capacity(8),
cuckoo.GrowthFactor(2), cuckoo.GrowthFactor(2),
) )
@@ -85,7 +85,7 @@ func TestResizeCapacity(t *testing.T) {
func TestPutMany(t *testing.T) { func TestPutMany(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
expected, actual := map[int]bool{}, cuckoo.NewTable[int, bool]() expected, actual := map[int]bool{}, cuckoo.New[int, bool]()
for i := range 1_000 { for i := range 1_000 {
expected[i] = true expected[i] = true
@@ -100,7 +100,7 @@ func TestPutMany(t *testing.T) {
func TestGetMany(t *testing.T) { func TestGetMany(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTable[int, bool]() table := cuckoo.New[int, bool]()
for i := range 1_000 { for i := range 1_000 {
err := table.Put(i, true) err := table.Put(i, true)
@@ -108,12 +108,12 @@ func TestGetMany(t *testing.T) {
} }
for i := range 2_000 { for i := range 2_000 {
value, err := table.Get(i) value, ok := table.Get(i)
if i < 1_000 { if i < 1_000 {
assert.NoError(err) assert.True(ok)
assert.Equal(value, true) assert.Equal(value, true)
} else { } else {
assert.Error(err) assert.False(ok)
} }
} }
} }
@@ -121,12 +121,12 @@ func TestGetMany(t *testing.T) {
func TestDropExistingItem(t *testing.T) { func TestDropExistingItem(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key, value := 0, true key, value := 0, true
table := cuckoo.NewTable[int, bool]() table := cuckoo.New[int, bool]()
(table.Put(key, value)) (table.Put(key, value))
err := table.Drop(key) had := table.Drop(key)
assert.NoError(err) assert.True(had)
assert.Equal(0, table.Size()) assert.Equal(0, table.Size())
assert.False(table.Has(key)) assert.False(table.Has(key))
} }
@@ -134,11 +134,11 @@ func TestDropExistingItem(t *testing.T) {
func TestDropNoItem(t *testing.T) { func TestDropNoItem(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key := 0 key := 0
table := cuckoo.NewTable[int, bool]() table := cuckoo.New[int, bool]()
err := table.Drop(key) had := table.Drop(key)
assert.NoError(err) assert.False(had)
assert.Equal(0, table.Size()) assert.Equal(0, table.Size())
assert.False(table.Has(key)) assert.False(table.Has(key))
} }
@@ -146,16 +146,15 @@ func TestDropNoItem(t *testing.T) {
func TestDropItemCapacity(t *testing.T) { func TestDropItemCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key := 0 key := 0
table := cuckoo.NewTable[int, bool]( table := cuckoo.New[int, bool](
cuckoo.Capacity(64), cuckoo.Capacity(64),
cuckoo.GrowthFactor(2), cuckoo.GrowthFactor(2),
) )
startingCapacity := table.TotalCapacity() startingCapacity := table.TotalCapacity()
err := table.Drop(key) table.Drop(key)
endingCapacity := table.TotalCapacity() endingCapacity := table.TotalCapacity()
assert.NoError(err)
assert.Equal(0, table.Size()) assert.Equal(0, table.Size())
assert.Equal(uint64(128), startingCapacity) assert.Equal(uint64(128), startingCapacity)
assert.Equal(uint64(64), endingCapacity) assert.Equal(uint64(64), endingCapacity)
@@ -164,7 +163,7 @@ func TestDropItemCapacity(t *testing.T) {
func TestPutNoCapacity(t *testing.T) { func TestPutNoCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
key, value := 0, true key, value := 0, true
table := cuckoo.NewTable[int, bool]( table := cuckoo.New[int, bool](
cuckoo.Capacity(0), cuckoo.Capacity(0),
) )
@@ -177,7 +176,7 @@ func TestPutNoCapacity(t *testing.T) {
func TestBadHashCapacity(t *testing.T) { func TestBadHashCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewCustomTable[int, bool]( table := cuckoo.NewCustom[int, bool](
func(int) uint64 { return 0 }, func(int) uint64 { return 0 },
func(int) uint64 { return 0 }, func(int) uint64 { return 0 },
func(a, b int) bool { return a == b }, func(a, b int) bool { return a == b },
@@ -197,15 +196,15 @@ func TestBadHashCapacity(t *testing.T) {
func TestDropResizeCapacity(t *testing.T) { func TestDropResizeCapacity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTable[int, bool]( table := cuckoo.New[int, bool](
cuckoo.Capacity(10), cuckoo.Capacity(10),
) )
err1 := table.Put(0, true) err1 := table.Put(0, true)
err2 := table.Put(1, true) err2 := table.Put(1, true)
err3 := table.Drop(1) table.Drop(1)
assert.NoError(errors.Join(err1, err2, err3)) assert.NoError(errors.Join(err1, err2))
assert.Equal(uint64(20), table.TotalCapacity()) assert.Equal(uint64(20), table.TotalCapacity())
} }
@@ -217,9 +216,7 @@ func TestNewTableBy(t *testing.T) {
} }
assert := assert.New(t) assert := assert.New(t)
table := cuckoo.NewTableBy[User, bool]( table := cuckoo.NewBy[User, bool](func(u User) string { return u.id })
func(u User) string { return u.id },
)
err := table.Put(User{nil, "1", "Robert"}, true) err := table.Put(User{nil, "1", "Robert"}, true)

7
doc.go
View File

@@ -1,9 +1,12 @@
// Package cuckoo provides a hash table that uses cuckoo hashing to achieve // Package cuckoo provides a hash table that uses cuckoo hashing to achieve
// a worst-case O(1) lookup time. // a worst-case O(1) lookup time.
// //
// While a [NewTable] only supports comparable keys by default, you can create // While a [New] only supports comparable keys by default, you can create
// a table with any key type using [NewCustomTable]. Custom [Hash] functions and // a table with any key type using [NewCustom]. Custom [Hash] functions and
// key comparison are also supported. // key comparison are also supported.
// //
// NOTE: The [Table] is a look-up structure, and not a source of truth. If
// [ErrBadHash] occurs, the data cannot be restored.
//
// See more: https://en.wikipedia.org/wiki/Cuckoo_hashing // See more: https://en.wikipedia.org/wiki/Cuckoo_hashing
package cuckoo package cuckoo

View File

@@ -8,25 +8,25 @@ import (
) )
func Example_basic() { func Example_basic() {
table := cuckoo.NewTable[int, string]() table := cuckoo.New[int, string]()
if err := table.Put(1, "Hello, World!"); err != nil { if err := table.Put(1, "Hello, World!"); err != nil {
fmt.Println("Put error:", err) fmt.Println("Put error:", err)
} }
if item, err := table.Get(1); err != nil { if item, ok := table.Get(1); !ok {
fmt.Println("Error:", err) fmt.Println("Not Found 1!")
} else { } else {
fmt.Println("Found 1:", item) fmt.Println("Found 1:", item)
} }
if item, err := table.Get(0); err != nil { if item, ok := table.Get(0); !ok {
fmt.Println("Error:", err) fmt.Println("Not Found 0!")
} else { } else {
fmt.Println("Found 0:", item) fmt.Println("Found 0:", item)
} }
// Output: // Output:
// Found 1: Hello, World! // Found 1: Hello, World!
// Error: key '0' not found // Not Found 0!
} }

View File

@@ -9,14 +9,20 @@ import "fmt"
const DefaultCapacity uint64 = 16 const DefaultCapacity uint64 = 16
// DefaultGrowthFactor is the standard resize multiplier for a [Table]. Most // DefaultGrowthFactor is the standard resize multiplier for a [Table]. Most
// hash table implementations use 2. // implementations use 2.
const DefaultGrowthFactor uint64 = 2 const DefaultGrowthFactor uint64 = 2
// DefaultMinimumLoad is the default lowest acceptable occupancy of a [Table]. // defaultMinimumLoad is the default lowest acceptable occupancy of a [Table].
// The value of 5% is taken from [libcuckoo]. // The higher the minimum load, the more likely that a [Table.Put] will not
// succeed. The value of 5% is taken from [libcuckoo].
// //
// [libcuckoo]: https://github.com/efficient/libcuckoo/blob/656714705a055df2b7a605eb3c71586d9da1e119/libcuckoo/cuckoohash_config.hh#L21 // [libcuckoo]: https://github.com/efficient/libcuckoo/blob/656714705a055df2b7a605eb3c71586d9da1e119/libcuckoo/cuckoohash_config.hh#L21
const DefaultMinimumLoad float64 = 0.05 const defaultMinimumLoad float64 = 0.05
// defaultGrowthLimit is the maximum number of times a [Table] can grow in a
// single [Table.Put], before the library infers it will lead to a stack
// overflow. The value of '64' was chosen arbirarily.
const defaultGrowthLimit uint64 = 64
type settings struct { type settings struct {
growthFactor uint64 growthFactor uint64
@@ -25,10 +31,10 @@ type settings struct {
} }
// An Option modifies the settings of a [Table]. It is used in its constructors // An Option modifies the settings of a [Table]. It is used in its constructors
// like [NewTable], for example. // like [New], for example.
type Option func(*settings) type Option func(*settings)
// Capacity modifies the starting capacity of each bucket of the [Table]. The // Capacity modifies the starting capacity of each subtable of the [Table]. The
// value must be non-negative. // value must be non-negative.
func Capacity(value int) Option { func Capacity(value int) Option {
if value < 0 { if value < 0 {
@@ -38,19 +44,6 @@ func Capacity(value int) Option {
return func(s *settings) { s.bucketSize = uint64(value) } return func(s *settings) { s.bucketSize = uint64(value) }
} }
// MinimumLoad modifies the [DefaultMinimumLoad] of the [Table]. The value must
// be between 0.00 and 1.00.
//
// The higher the minimum load, the more likely that a [Table.Put] will not
// succeed. Minimum loads above 20% are not tested.
func MinimumLoad(value float64) Option {
if value < 0.00 || value > 1.00 {
panic(fmt.Sprintf("go-cuckoo: MinimumLoad must be between 0.00 and 1.00, got %f", value))
}
return func(s *settings) { s.minLoadFactor = value }
}
// GrowthFactor controls how much the capacity of the [Table] multiplies when // GrowthFactor controls how much the capacity of the [Table] multiplies when
// it must resize. The value must be greater than 1. // it must resize. The value must be greater than 1.
func GrowthFactor(value int) Option { func GrowthFactor(value int) Option {

107
subtable.go Normal file
View File

@@ -0,0 +1,107 @@
package cuckoo
// An entry is a key-value pair.
type entry[K, V any] struct {
key K
value V
}
type slot[K, V any] struct {
entry[K, V]
occupied bool
}
type subtable[K, V any] struct {
hash Hash[K]
slots []slot[K, V]
capacity, size uint64
compare EqualFunc[K]
}
// location determines where in the subtable a certain key would be placed. If
// the capacity is 0, this will panic.
func (t *subtable[K, V]) location(key K) uint64 {
return t.hash(key) % t.capacity
}
func (t *subtable[K, V]) get(key K) (value V, found bool) {
if t.capacity == 0 {
return
}
slot := t.slots[t.location(key)]
return slot.value, slot.occupied && t.compare(slot.key, key)
}
func (t *subtable[K, V]) drop(key K) (occupied bool) {
if t.capacity == 0 {
return
}
slot := &t.slots[t.location(key)]
if slot.occupied && t.compare(slot.key, key) {
slot.occupied = false
t.size--
return true
}
return false
}
func (t *subtable[K, V]) resized(capacity uint64) *subtable[K, V] {
return &subtable[K, V]{
slots: make([]slot[K, V], capacity),
capacity: capacity,
hash: t.hash,
compare: t.compare,
}
}
func (t *subtable[K, V]) update(key K, value V) (updated bool) {
if t.capacity == 0 {
return
}
slot := &t.slots[t.location(key)]
if slot.occupied && t.compare(slot.key, key) {
slot.value = value
return true
}
return false
}
func (t *subtable[K, V]) insert(insertion entry[K, V]) (evicted entry[K, V], eviction bool) {
if t.capacity == 0 {
return insertion, true
}
slot := &t.slots[t.location(insertion.key)]
if !slot.occupied {
slot.entry = insertion
slot.occupied = true
t.size++
return
}
if t.compare(slot.key, insertion.key) {
slot.value = insertion.value
return
}
insertion, slot.entry = slot.entry, insertion
return insertion, true
}
func newSubtable[K, V any](capacity uint64, hash Hash[K], compare EqualFunc[K]) *subtable[K, V] {
return &subtable[K, V]{
hash: hash,
capacity: capacity,
compare: compare,
size: 0,
slots: make([]slot[K, V], capacity),
}
}

226
table.go
View File

@@ -1,41 +1,50 @@
package cuckoo package cuckoo
import ( import (
"errors"
"fmt" "fmt"
"iter" "iter"
"math/bits" "math/bits"
"strings" "strings"
) )
// A Table is hash table that uses cuckoo hashing to resolve collision. Create // ErrBadHash occurs when the hashes given to a [Table] cause too many key
// one with [NewTable]. Or if you want more granularity, use [NewTableBy] or // collisions. Discard the old table, rebuild it from your source data, and try:
// [NewCustomTable]. //
// 1. Different hash seeds. Equal seeds produce equal hash functions, which
// always cycle.
// 2. A different [Hash] algorithm.
var ErrBadHash = errors.New("bad hash")
// A Table which uses cuckoo hashing to resolve collision. Create
// one with [New]. Or if you want more granularity, use [NewBy] or
// [NewCustom].
type Table[K, V any] struct { type Table[K, V any] struct {
bucketA, bucketB bucket[K, V] tableA, tableB *subtable[K, V]
growthFactor uint64 growthFactor uint64
minLoadFactor float64 minLoadFactor float64
} }
// TotalCapacity returns the number of slots allocated for the [Table]. To get the // TotalCapacity returns the number of slots allocated for the [Table]. To get the
// number of slots filled, look at [Table.Size]. // number of slots filled, look at [Table.Size].
func (t Table[K, V]) TotalCapacity() uint64 { func (t *Table[K, V]) TotalCapacity() uint64 {
return t.bucketA.capacity + t.bucketB.capacity return t.tableA.capacity + t.tableB.capacity
} }
// Size returns how many slots are filled in the [Table]. // Size returns how many slots are filled in the [Table].
func (t Table[K, V]) Size() int { func (t *Table[K, V]) Size() int {
return int(t.bucketA.size + t.bucketB.size) return int(t.tableA.size + t.tableB.size)
} }
func log2(n uint64) (m int) { func log2(n uint64) (m int) {
return max(0, bits.Len64(n)-1) return max(0, bits.Len64(n)-1)
} }
func (t Table[K, V]) maxEvictions() int { func (t *Table[K, V]) maxEvictions() int {
return 3 * log2(t.TotalCapacity()) return 3 * log2(t.TotalCapacity())
} }
func (t Table[K, V]) load() float64 { func (t *Table[K, V]) load() float64 {
// When there are no slots in the table, we still treat the load as 100%. // When there are no slots in the table, we still treat the load as 100%.
// Every slot in the table is full. // Every slot in the table is full.
if t.TotalCapacity() == 0 { if t.TotalCapacity() == 0 {
@@ -45,116 +54,153 @@ func (t Table[K, V]) load() float64 {
return float64(t.Size()) / float64(t.TotalCapacity()) return float64(t.Size()) / float64(t.TotalCapacity())
} }
// resize clears all buckets, changes the sizes of them to a specific capacity, // insert attempts to put/update an entry in the table, without modifying the
// and fills them back up again. It is a helper function for [Table.grow] and // size of the table. Returns a displaced entry and 'homeless = true' if an
// [Table.shrink]; use them instead. // entry could not be placed after exhausting evictions.
func (t *Table[K, V]) resize(capacity uint64) error { func (t *Table[K, V]) insert(entry entry[K, V]) (displaced entry[K, V], homeless bool) {
entries := make([]entry[K, V], 0, t.Size()) if t.tableA.update(entry.key, entry.value) {
return
}
if t.tableB.update(entry.key, entry.value) {
return
}
for range t.maxEvictions() {
if entry, homeless = t.tableA.insert(entry); !homeless {
return
}
if entry, homeless = t.tableB.insert(entry); !homeless {
return
}
}
return entry, true
}
// resized creates an empty copy of the table, with a new capacity for each
// bucket.
func (t *Table[K, V]) resized(capacity uint64) *Table[K, V] {
return &Table[K, V]{
growthFactor: t.growthFactor,
minLoadFactor: t.minLoadFactor,
tableA: t.tableA.resized(capacity),
tableB: t.tableB.resized(capacity),
}
}
// resize creates a new [Table.resized] with 'capacity', inserts all items into
// the array, and replaces the current table. It is a helper function for
// [Table.grow] and [Table.shrink]; use them instead.
func (t *Table[K, V]) resize(capacity uint64) bool {
updated := t.resized(capacity)
for k, v := range t.Entries() { for k, v := range t.Entries() {
entries = append(entries, entry[K, V]{k, v}) if _, failed := updated.insert(entry[K, V]{k, v}); failed {
} return false
t.bucketA.resize(capacity)
t.bucketB.resize(capacity)
for _, entry := range entries {
if err := t.Put(entry.key, entry.value); err != nil {
return err
} }
} }
return nil *t = *updated
return true
} }
// grow increases the table's capacity by the [Table.growthFactor]. If the // grow increases the table's capacity by the growth factor. If the
// capacity is 0, it increases it to 1. // capacity is 0, it increases it to 1.
func (t *Table[K, V]) grow() error { func (t *Table[K, V]) grow() bool {
var newCapacity uint64 var newCapacity uint64
if t.TotalCapacity() == 0 { if t.TotalCapacity() == 0 {
newCapacity = 1 newCapacity = 1
} else { } else {
newCapacity = t.bucketA.capacity * t.growthFactor newCapacity = t.tableA.capacity * t.growthFactor
} }
return t.resize(newCapacity) return t.resize(newCapacity)
} }
// shrink reduces the table's capacity by the [Table.growthFactor]. It may // shrink reduces the table's capacity by the growth factor. It may
// reduce it down to 0. // reduce it down to 0.
func (t *Table[K, V]) shrink() error { func (t *Table[K, V]) shrink() bool {
return t.resize(t.bucketA.capacity / t.growthFactor) return t.resize(t.tableA.capacity / t.growthFactor)
} }
// Get fetches the value for a key in the [Table]. Returns an error if no value // Get fetches the value for a key in the [Table]. Matches the comma-ok pattern
// is found. // of a builtin map; see [Table.Find] for plain indexing.
func (t Table[K, V]) Get(key K) (value V, err error) { func (t *Table[K, V]) Get(key K) (value V, ok bool) {
if item, ok := t.bucketA.get(key); ok { if item, ok := t.tableA.get(key); ok {
return item, nil return item, true
} }
if item, ok := t.bucketB.get(key); ok { if item, ok := t.tableB.get(key); ok {
return item, nil return item, true
} }
return value, fmt.Errorf("key '%v' not found", key) return
}
// Find fetches the value of a key. Matches direct indexing of a builtin map;
// see [Table.Get] for a comma-ok pattern.
func (t *Table[K, V]) Find(key K) (value V) {
value, _ = t.Get(key)
return
} }
// Has returns true if a key has a value in the table. // Has returns true if a key has a value in the table.
func (t Table[K, V]) Has(key K) (exists bool) { func (t *Table[K, V]) Has(key K) (exists bool) {
_, err := t.Get(key) _, exists = t.Get(key)
return err == nil return
} }
// Put sets the value for a key. Returns error if its value cannot be set. // Put sets the value for a key. If it cannot be set, an error is returned.
func (t *Table[K, V]) Put(key K, value V) (err error) { func (t *Table[K, V]) Put(key K, value V) (err error) {
if t.bucketA.update(key, value) { var (
return nil entry = entry[K, V]{key, value}
homeless bool
)
for range defaultGrowthLimit {
if entry, homeless = t.insert(entry); !homeless {
return
} }
if t.bucketB.update(key, value) { // Both this and the growth limit are necessary: this catches bad hashes
return nil // early when the table is sparse, while the latter catches cases where
// growing never helps.
if t.load() < t.minLoadFactor {
return fmt.Errorf("hash functions produced a cycle at load %d/%d: %w", t.Size(), t.TotalCapacity(), ErrBadHash)
} }
entry, eviction := entry[K, V]{key, value}, false // It is theoretically possible to have a table with a larger capacity
for range t.maxEvictions() { // that is valid. But this chance is astronomically small, so we ignore
if entry, eviction = t.bucketA.evict(entry); !eviction { // it in this implementation.
return nil if grew := t.grow(); !grew {
return fmt.Errorf("could not redistribute entries into larger table: %w", ErrBadHash)
}
} }
if entry, eviction = t.bucketB.evict(entry); !eviction { return fmt.Errorf("could not place entry after %d resizes: %w", defaultGrowthLimit, ErrBadHash)
return nil
}
} }
// Drop removes a value for a key in the table. Returns whether the key had
// existed.
func (t *Table[K, V]) Drop(key K) bool {
occupied := t.tableA.drop(key) || t.tableB.drop(key)
if t.load() < t.minLoadFactor { if t.load() < t.minLoadFactor {
return fmt.Errorf("bad hash: resize on load %d/%d = %f", t.Size(), t.TotalCapacity(), t.load()) // The error is not handled here, because table-shrinking is an internal
// optimization.
t.shrink()
} }
if err := t.grow(); err != nil { return occupied
return err
}
return t.Put(entry.key, entry.value)
}
// Drop removes a value for a key in the table. Returns an error if its value
// cannot be removed.
func (t *Table[K, V]) Drop(key K) (err error) {
t.bucketA.drop(key)
t.bucketB.drop(key)
if t.load() < t.minLoadFactor {
return t.shrink()
}
return nil
} }
// Entries returns an unordered sequence of all key-value pairs in the table. // Entries returns an unordered sequence of all key-value pairs in the table.
func (t Table[K, V]) Entries() iter.Seq2[K, V] { func (t *Table[K, V]) Entries() iter.Seq2[K, V] {
return func(yield func(K, V) bool) { return func(yield func(K, V) bool) {
for _, slot := range t.bucketA.slots { for _, slot := range t.tableA.slots {
if slot.occupied { if slot.occupied {
if !yield(slot.key, slot.value) { if !yield(slot.key, slot.value) {
return return
@@ -162,7 +208,7 @@ func (t Table[K, V]) Entries() iter.Seq2[K, V] {
} }
} }
for _, slot := range t.bucketB.slots { for _, slot := range t.tableB.slots {
if slot.occupied { if slot.occupied {
if !yield(slot.key, slot.value) { if !yield(slot.key, slot.value) {
return return
@@ -173,8 +219,8 @@ func (t Table[K, V]) Entries() iter.Seq2[K, V] {
} }
// String returns the entries of the table as a string in the format: // String returns the entries of the table as a string in the format:
// "table[k1:v1 h2:v2 ...]". // "table[k1:v1 k2:v2 ...]".
func (t Table[K, V]) String() string { func (t *Table[K, V]) String() string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("table[") sb.WriteString("table[")
@@ -192,13 +238,13 @@ func (t Table[K, V]) String() string {
return sb.String() return sb.String()
} }
// NewCustomTable creates a [Table] with custom [Hash] and [EqualFunc] // NewCustom creates a [Table] with custom [Hash] and [EqualFunc]
// functions, along with any [Option] the user provides. // functions, along with any [Option] the user provides.
func NewCustomTable[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], options ...Option) *Table[K, V] { func NewCustom[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], options ...Option) *Table[K, V] {
settings := &settings{ settings := &settings{
growthFactor: DefaultGrowthFactor, growthFactor: DefaultGrowthFactor,
bucketSize: DefaultCapacity, bucketSize: DefaultCapacity,
minLoadFactor: DefaultMinimumLoad, minLoadFactor: defaultMinimumLoad,
} }
for _, option := range options { for _, option := range options {
@@ -208,8 +254,8 @@ func NewCustomTable[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], option
return &Table[K, V]{ return &Table[K, V]{
growthFactor: settings.growthFactor, growthFactor: settings.growthFactor,
minLoadFactor: settings.minLoadFactor, minLoadFactor: settings.minLoadFactor,
bucketA: newBucket[K, V](settings.bucketSize, hashA, compare), tableA: newSubtable[K, V](settings.bucketSize, hashA, compare),
bucketB: newBucket[K, V](settings.bucketSize, hashB, compare), tableB: newSubtable[K, V](settings.bucketSize, hashB, compare),
} }
} }
@@ -217,10 +263,10 @@ func pipe[X, Y, Z any](a func(X) Y, b func(Y) Z) func(X) Z {
return func(x X) Z { return b(a(x)) } return func(x X) Z { return b(a(x)) }
} }
// NewTableBy creates a [Table] for any key type by using keyFunc to derive a // NewBy creates a [Table] for any key type by using keyFunc to derive a
// comparable key. Two keys with the same derived key are treated as equal. // comparable key. Two keys with the same derived key are treated as equal.
func NewTableBy[K, V any, C comparable](keyFunc func(K) C, options ...Option) *Table[K, V] { func NewBy[K, V any, C comparable](keyFunc func(K) C, options ...Option) *Table[K, V] {
return NewCustomTable[K, V]( return NewCustom[K, V](
pipe(keyFunc, NewDefaultHash[C]()), pipe(keyFunc, NewDefaultHash[C]()),
pipe(keyFunc, NewDefaultHash[C]()), pipe(keyFunc, NewDefaultHash[C]()),
func(a, b K) bool { return keyFunc(a) == keyFunc(b) }, func(a, b K) bool { return keyFunc(a) == keyFunc(b) },
@@ -228,10 +274,10 @@ func NewTableBy[K, V any, C comparable](keyFunc func(K) C, options ...Option) *T
) )
} }
// NewTable creates a [Table] using the default [Hash] and [EqualFunc]. Use // New creates a [Table] using the default [Hash] and [EqualFunc]. Use
// the [Option] functions to configure its behavior. Note that this constructor // the [Option] functions to configure its behavior. Note that this constructor
// is only provided for comparable keys. For arbitrary keys, consider // is only provided for comparable keys. For arbitrary keys, consider
// [NewTableBy] or [NewCustomTable]. // [NewBy] or [NewCustom].
func NewTable[K comparable, V any](options ...Option) *Table[K, V] { func New[K comparable, V any](options ...Option) *Table[K, V] {
return NewCustomTable[K, V](NewDefaultHash[K](), NewDefaultHash[K](), DefaultEqualFunc[K], options...) return NewCustom[K, V](NewDefaultHash[K](), NewDefaultHash[K](), DefaultEqualFunc[K], options...)
} }