refactor!: shorter constructors, bucket → subtable
#22
@@ -1,11 +1,11 @@
|
||||
package cuckoo
|
||||
|
||||
// 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,
|
||||
// deterministic, and fast. By default, [NewTable] uses [DefaultEqualFunc].
|
||||
// are teated as the same by the [HashTable]. A good EqualFunc is pure,
|
||||
// deterministic, and fast. By default, [New] uses [DefaultEqualFunc].
|
||||
//
|
||||
// This function MUST NOT return true if the [Hash] digest of two keys
|
||||
// are different: the [Table] will not work.
|
||||
// are different: the [HashTable] will not work.
|
||||
type EqualFunc[K any] = func(a, b K) bool
|
||||
|
||||
// DefaultEqualFunc compares two keys by strict equality. Returns true if the
|
||||
|
||||
@@ -28,7 +28,7 @@ func ExampleEqualFunc_badEqualFunc() {
|
||||
// Two users with the same ID are equal.
|
||||
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))
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func FuzzInsertLookup(f *testing.F) {
|
||||
fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d\n",
|
||||
seedA, seedB, capacity, growthFactor)
|
||||
|
||||
actual := cuckoo.NewCustomTable[uint32, uint32](
|
||||
actual := cuckoo.NewCustom[uint32, uint32](
|
||||
offsetHash(seedA),
|
||||
offsetHash(seedB),
|
||||
func(a, b uint32) bool { return a == b },
|
||||
|
||||
@@ -11,7 +11,7 @@ func TestMaxEvictions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
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)))
|
||||
|
||||
assert.Equal(table.maxEvictions(), int(expectedEvictions))
|
||||
@@ -20,7 +20,7 @@ func TestMaxEvictions(t *testing.T) {
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
table := NewTable[int, bool](Capacity(8))
|
||||
table := New[int, bool](Capacity(8))
|
||||
|
||||
for i := range 16 {
|
||||
err := table.Put(i, true)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func TestNewTable(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
table := cuckoo.NewTable[int, bool]()
|
||||
table := cuckoo.New[int, bool]()
|
||||
|
||||
assert.NotNil(table)
|
||||
assert.Zero(table.Size())
|
||||
@@ -23,7 +23,7 @@ func TestNewTable(t *testing.T) {
|
||||
func TestAddItem(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key, value := 0, true
|
||||
table := cuckoo.NewTable[int, bool]()
|
||||
table := cuckoo.New[int, bool]()
|
||||
|
||||
err := table.Put(key, value)
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestAddItem(t *testing.T) {
|
||||
func TestPutOverwrite(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key, value, newValue := 0, 1, 2
|
||||
table := cuckoo.NewTable[int, int]()
|
||||
table := cuckoo.New[int, int]()
|
||||
(table.Put(key, value))
|
||||
|
||||
err := table.Put(key, newValue)
|
||||
@@ -50,7 +50,7 @@ func TestPutOverwrite(t *testing.T) {
|
||||
func TestSameHash(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
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)
|
||||
errB := table.Put(1, true)
|
||||
@@ -63,14 +63,14 @@ func TestSameHash(t *testing.T) {
|
||||
|
||||
func TestStartingCapacity(t *testing.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())
|
||||
}
|
||||
|
||||
func TestResizeCapacity(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
table := cuckoo.NewTable[int, bool](
|
||||
table := cuckoo.New[int, bool](
|
||||
cuckoo.Capacity(8),
|
||||
cuckoo.GrowthFactor(2),
|
||||
)
|
||||
@@ -85,7 +85,7 @@ func TestResizeCapacity(t *testing.T) {
|
||||
|
||||
func TestPutMany(t *testing.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 {
|
||||
expected[i] = true
|
||||
@@ -100,7 +100,7 @@ func TestPutMany(t *testing.T) {
|
||||
|
||||
func TestGetMany(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
table := cuckoo.NewTable[int, bool]()
|
||||
table := cuckoo.New[int, bool]()
|
||||
|
||||
for i := range 1_000 {
|
||||
err := table.Put(i, true)
|
||||
@@ -121,7 +121,7 @@ func TestGetMany(t *testing.T) {
|
||||
func TestDropExistingItem(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key, value := 0, true
|
||||
table := cuckoo.NewTable[int, bool]()
|
||||
table := cuckoo.New[int, bool]()
|
||||
(table.Put(key, value))
|
||||
|
||||
err := table.Drop(key)
|
||||
@@ -134,7 +134,7 @@ func TestDropExistingItem(t *testing.T) {
|
||||
func TestDropNoItem(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key := 0
|
||||
table := cuckoo.NewTable[int, bool]()
|
||||
table := cuckoo.New[int, bool]()
|
||||
|
||||
err := table.Drop(key)
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestDropNoItem(t *testing.T) {
|
||||
func TestDropItemCapacity(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key := 0
|
||||
table := cuckoo.NewTable[int, bool](
|
||||
table := cuckoo.New[int, bool](
|
||||
cuckoo.Capacity(64),
|
||||
cuckoo.GrowthFactor(2),
|
||||
)
|
||||
@@ -164,7 +164,7 @@ func TestDropItemCapacity(t *testing.T) {
|
||||
func TestPutNoCapacity(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
key, value := 0, true
|
||||
table := cuckoo.NewTable[int, bool](
|
||||
table := cuckoo.New[int, bool](
|
||||
cuckoo.Capacity(0),
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ func TestPutNoCapacity(t *testing.T) {
|
||||
|
||||
func TestBadHashCapacity(t *testing.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(a, b int) bool { return a == b },
|
||||
@@ -197,7 +197,7 @@ func TestBadHashCapacity(t *testing.T) {
|
||||
|
||||
func TestDropResizeCapacity(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
table := cuckoo.NewTable[int, bool](
|
||||
table := cuckoo.New[int, bool](
|
||||
cuckoo.Capacity(10),
|
||||
)
|
||||
|
||||
@@ -217,9 +217,7 @@ func TestNewTableBy(t *testing.T) {
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
table := cuckoo.NewTableBy[User, bool](
|
||||
func(u User) string { return u.id },
|
||||
)
|
||||
table := cuckoo.NewBy[User, bool](func(u User) string { return u.id })
|
||||
|
||||
err := table.Put(User{nil, "1", "Robert"}, true)
|
||||
|
||||
|
||||
4
doc.go
4
doc.go
@@ -1,8 +1,8 @@
|
||||
// Package cuckoo provides a hash table that uses cuckoo hashing to achieve
|
||||
// a worst-case O(1) lookup time.
|
||||
//
|
||||
// While a [NewTable] only supports comparable keys by default, you can create
|
||||
// a table with any key type using [NewCustomTable]. Custom [Hash] functions and
|
||||
// While a [New] only supports comparable keys by default, you can create
|
||||
// a table with any key type using [NewCustom]. Custom [Hash] functions and
|
||||
// key comparison are also supported.
|
||||
//
|
||||
// See more: https://en.wikipedia.org/wiki/Cuckoo_hashing
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func Example_basic() {
|
||||
table := cuckoo.NewTable[int, string]()
|
||||
table := cuckoo.New[int, string]()
|
||||
|
||||
if err := table.Put(1, "Hello, World!"); err != nil {
|
||||
fmt.Println("Put error:", err)
|
||||
|
||||
4
hash.go
4
hash.go
@@ -7,9 +7,9 @@ import (
|
||||
// A Hash function maps any data to a fixed-length value (in this case, a
|
||||
// [uint64]).
|
||||
//
|
||||
// It is used by the [Table] to evenly distribute values
|
||||
// It is used by the [HashTable] to evenly distribute values
|
||||
// amongst its slots. A good hash function is uniform, [chaotic], and
|
||||
// deterministic. [Table] uses [NewDefaultHash] by default, which is built on
|
||||
// deterministic. [HashTable] uses [NewDefaultHash] by default, which is built on
|
||||
// [maphash.Comparable].
|
||||
//
|
||||
// [chaotic]: https://en.wikipedia.org/wiki/Avalanche_effect
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A HashTable is hash table that uses cuckoo hashing to resolve collision. Create
|
||||
// one with [NewTable]. Or if you want more granularity, use [NewTableBy] or
|
||||
// [NewCustomTable].
|
||||
// A HashTable which uses cuckoo hashing to resolve collision. Create
|
||||
// one with [New]. Or if you want more granularity, use [NewBy] or
|
||||
// [NewCustom].
|
||||
type HashTable[K, V any] struct {
|
||||
tableA, tableB table[K, V]
|
||||
growthFactor uint64
|
||||
@@ -192,9 +192,9 @@ func (t *HashTable[K, V]) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// NewCustomTable creates a [HashTable] with custom [Hash] and [EqualFunc]
|
||||
// NewCustom creates a [HashTable] with custom [Hash] and [EqualFunc]
|
||||
// functions, along with any [Option] the user provides.
|
||||
func NewCustomTable[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], options ...Option) *HashTable[K, V] {
|
||||
func NewCustom[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], options ...Option) *HashTable[K, V] {
|
||||
settings := &settings{
|
||||
growthFactor: DefaultGrowthFactor,
|
||||
bucketSize: DefaultCapacity,
|
||||
@@ -217,10 +217,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)) }
|
||||
}
|
||||
|
||||
// NewTableBy creates a [HashTable] for any key type by using keyFunc to derive a
|
||||
// NewBy creates a [HashTable] for any key type by using keyFunc to derive a
|
||||
// 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) *HashTable[K, V] {
|
||||
return NewCustomTable[K, V](
|
||||
func NewBy[K, V any, C comparable](keyFunc func(K) C, options ...Option) *HashTable[K, V] {
|
||||
return NewCustom[K, V](
|
||||
pipe(keyFunc, NewDefaultHash[C]()),
|
||||
pipe(keyFunc, NewDefaultHash[C]()),
|
||||
func(a, b K) bool { return keyFunc(a) == keyFunc(b) },
|
||||
@@ -228,10 +228,10 @@ func NewTableBy[K, V any, C comparable](keyFunc func(K) C, options ...Option) *H
|
||||
)
|
||||
}
|
||||
|
||||
// NewTable creates a [HashTable] using the default [Hash] and [EqualFunc]. Use
|
||||
// New creates a [HashTable] using the default [Hash] and [EqualFunc]. Use
|
||||
// the [Option] functions to configure its behavior. Note that this constructor
|
||||
// is only provided for comparable keys. For arbitrary keys, consider
|
||||
// [NewTableBy] or [NewCustomTable].
|
||||
func NewTable[K comparable, V any](options ...Option) *HashTable[K, V] {
|
||||
return NewCustomTable[K, V](NewDefaultHash[K](), NewDefaultHash[K](), DefaultEqualFunc[K], options...)
|
||||
// [NewBy] or [NewCustom].
|
||||
func New[K comparable, V any](options ...Option) *HashTable[K, V] {
|
||||
return NewCustom[K, V](NewDefaultHash[K](), NewDefaultHash[K](), DefaultEqualFunc[K], options...)
|
||||
}
|
||||
|
||||
18
settings.go
18
settings.go
@@ -2,18 +2,18 @@ package cuckoo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DefaultCapacity is the initial capacity of a [Table]. It is inspired from
|
||||
// DefaultCapacity is the initial capacity of a [HashTable]. It is inspired from
|
||||
// Java's [HashMap] implementation, which also uses 16.
|
||||
//
|
||||
// [HashMap]: https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html#HashMap--
|
||||
const DefaultCapacity uint64 = 16
|
||||
|
||||
// DefaultGrowthFactor is the standard resize multiplier for a [Table]. Most
|
||||
// hash table implementations use 2.
|
||||
// DefaultGrowthFactor is the standard resize multiplier for a [HashTable]. Most
|
||||
// implementations use 2.
|
||||
const DefaultGrowthFactor uint64 = 2
|
||||
|
||||
// defaultMinimumLoad is the default lowest acceptable occupancy of a [Table].
|
||||
// The higher the minimum load, the more likely that a [Table.Put] will not
|
||||
// defaultMinimumLoad is the default lowest acceptable occupancy of a [HashTable].
|
||||
// The higher the minimum load, the more likely that a [HashTable.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
|
||||
@@ -25,11 +25,11 @@ type settings struct {
|
||||
bucketSize uint64
|
||||
}
|
||||
|
||||
// An Option modifies the settings of a [Table]. It is used in its constructors
|
||||
// like [NewTable], for example.
|
||||
// An Option modifies the settings of a [HashTable]. It is used in its constructors
|
||||
// like [New], for example.
|
||||
type Option func(*settings)
|
||||
|
||||
// Capacity modifies the starting capacity of each bucket of the [Table]. The
|
||||
// Capacity modifies the starting capacity of each bucket of the [HashTable]. The
|
||||
// value must be non-negative.
|
||||
func Capacity(value int) Option {
|
||||
if value < 0 {
|
||||
@@ -39,7 +39,7 @@ func Capacity(value int) Option {
|
||||
return func(s *settings) { s.bucketSize = uint64(value) }
|
||||
}
|
||||
|
||||
// GrowthFactor controls how much the capacity of the [Table] multiplies when
|
||||
// GrowthFactor controls how much the capacity of the [HashTable] multiplies when
|
||||
// it must resize. The value must be greater than 1.
|
||||
func GrowthFactor(value int) Option {
|
||||
if value < 2 {
|
||||
|
||||
Reference in New Issue
Block a user