docs: congruency target

This commit is contained in:
2026-05-09 16:57:58 -04:00
parent 5c84ed7794
commit bd25bb69bd

View File

@@ -1,21 +1,21 @@
# Designing an Idiomatic Interface # Designing an Idiomatic API Interface
Currently, the contract for package was built without design. We (the maintainers) built `go-cuckoo`'s API interface without design intent.
More attention was paid to implementing the underlying functionality of the cuckoo hashing. Up until now, we paid more attention implementing the underlying functionality of the cuckoo hashing.
With the fundamentals of the algorithm built, our API contract should be revisited. With the fundamentals of the algorithm built, we should revisit the interface.
It should align closer to the following principles: It should align closer to the following principles:
- **Congruency to the builtin map.** - **Congruency**
Our cuckoo table should have the same core functionality as Go's built-in map. A `go-cuckoo` table should have the same core functionality as Go's built-in map.
- **Familiarity to the builtin map.** - **Familiarity**
If our cuckoo table behaves similarly to Go's standard map, our user will intuitively know how to use it. A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it.
This lowers the cognitive load our developers must carry. In effect, its users will carry less cognitive load.
## Current State ## Current State
### Interface of the Builtin Map ### Interface of the built-in Map
Listed below is every interface provided by Go to the built-in map object. Listed below is every interface provided by Go to the built-in map object.
Also included, are the functions from the package `maps` in the standard library. Also included, are the functions from the package `maps` in the standard library.
@@ -23,7 +23,7 @@ Also included, are the functions from the package `maps` in the standard library
<details> <details>
<summary>Interfaces</summary> <summary>Interfaces</summary>
| # | Builtin Interface | Description | | # | built-in Interface | Description |
| --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `m := make(map[K]V)` | Returns an empty map using the built-in `make()` function. | | 1 | `m := make(map[K]V)` | Returns an empty map using the built-in `make()` function. |
| 2 | `m := make(map[K]V, hint)` | Returns an empty map using `make()`, with a capacity 'hint'. This hint is how many items the map expects to hold, _not_ a measure of how large it is. | | 2 | `m := make(map[K]V, hint)` | Returns an empty map using `make()`, with a capacity 'hint'. This hint is how many items the map expects to hold, _not_ a measure of how large it is. |
@@ -56,7 +56,7 @@ On the other hand, here is the current contract for `go-cuckoo`.
<details> <details>
<summary>Interfaces</summary> <summary>Interfaces</summary>
| # | Builtin Interface | Description | | # | built-in Interface | Description |
| --- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | --- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| 1 | `m := New(opts...)` | Creates a table using the default hash and equal function. The options configure its behavior. Confined to comparable keys. | | 1 | `m := New(opts...)` | Creates a table using the default hash and equal function. The options configure its behavior. Confined to comparable keys. |
| 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | | 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. |
@@ -311,34 +311,22 @@ There is no analog.
### Solving Congruency ### Solving Congruency
The following changes will be made to accomodate for congruency: We should make the following changes to accomodate for congruency:
<details> <details>
<summary><code>ok := maps.EqualFunc(m1, m2, fn)</code></summary> <summary><code>ok := maps.EqualFunc(m1, m2, fn)</code></summary>
To solve this, we need a new function: We should implement a new function:
```go ```go
func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool { func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool
if t1.Size() != t2.Size() {
return false
}
for k, v1 := range t1.Entries() {
if v2, ok := t2.Get(k); !ok || eq(v1, v2) {
return false
}
}
return true
}
``` ```
This function is free, and not bound as a receiver function. This function is free, and not bound as a receiver function.
(It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) (It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.)
The latter implies `t1` has authority, when in fact neither do. The latter implies `t1` has authority, when in fact neither do.
Equality will be defined as: We define equality as:
1. Neither table has a key the other doesn't. 1. Neither table has a key the other doesn't.
2. Each key has the same value in each table. 2. Each key has the same value in each table.
@@ -353,22 +341,20 @@ So, we must assume that:
The name `EqualFunc` is already taken by `EqualFunc[K, V]`: an alias for `func(a, b K) bool`. The name `EqualFunc` is already taken by `EqualFunc[K, V]`: an alias for `func(a, b K) bool`.
Inlining `EqualFunc[K, V]` would solve this problem. Inlining `EqualFunc[K, V]` would solve this problem.
The documentation attached to it would be moved to `DefaultEqualFunc`. We will move the documentation attached to it to `DefaultEqualFunc`.
</details> </details>
<details> <details>
<summary><code>ok := maps.Equal(m1, m2)</code></summary> <summary><code>ok := maps.Equal(m1, m2)</code></summary>
The addition of `cuckoo.EqualFunc` makes an implementation trivial: We should implement a new function, to conform with the standard library:
```go ```go
func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool { func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool
return EqualFunc(t1, t2, DefaultEqualFunc[V])
}
``` ```
To conform with the standard library, a new function should be added. It uses the same equality check as in `EqualFunc`.
Once again, the function is free because it is symmetric. Once again, the function is free because it is symmetric.
</details> </details>
@@ -376,18 +362,10 @@ Once again, the function is free because it is symmetric.
<details> <details>
<summary><code>maps.Insert(m, seq)</code></summary> <summary><code>maps.Insert(m, seq)</code></summary>
This functionality requires a new receiver: We should implement a new receiver for the table:
```go ```go
func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error { func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error
for k, v := range seq {
if err := t.Put(k, v); err != nil {
return err
}
}
return nil
}
``` ```
A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric. A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric.
@@ -404,21 +382,20 @@ Ultimately, `t.Insert()` is a better choice to be consistent with `maps`.
<details> <details>
<summary><code>maps.Copy(dst, src)</code></summary> <summary><code>maps.Copy(dst, src)</code></summary>
To solve this, we must implement a new receiver. We should implement a new receiver for the table:
Luckily, `t.Insert` makes it trivial:
```go ```go
func (t *Table[K, V]) Copy(src *Table[K, V]) error { func (t *Table[K, V]) Copy(src *Table[K, V]) error
return t.Insert(src.Entries())
}
``` ```
It's functionality should match that of `t.Insert()`.
A receiver fits better even though `maps.Copy` is a free function, 'copying' it is asymmetric: `dst` is writen into by `src`. A receiver fits better even though `maps.Copy` is a free function, 'copying' it is asymmetric: `dst` is writen into by `src`.
It is only free because Go's standard map is built into the language, and so cannot have receivers. It is only free because Go's standard map is built into the language, and so cannot have receivers.
The name `t.Merge()` might be more accurate, but it does work because: The name `t.Merge()` might be more accurate, but it does work because:
- `t.Copy()` matches Go's builtin `copy()`, and `io.Copy()`. The Go team used [the same logic](https://github.com/golang/go/discussions/47330#discussioncomment-1167799) to name `maps.Copy()`. - `t.Copy()` matches Go's built-in `copy()`, and `io.Copy()`. The Go team used [the same logic](https://github.com/golang/go/discussions/47330#discussioncomment-1167799) to name `maps.Copy()`.
In this case, `t.Merge()` would be an outlier. In this case, `t.Merge()` would be an outlier.
- `t.Merge()` implies some sort of conflict-resolution, when there is not. - `t.Merge()` implies some sort of conflict-resolution, when there is not.
It simply overwrites the values. It simply overwrites the values.
@@ -428,16 +405,10 @@ The name `t.Merge()` might be more accurate, but it does work because:
<details> <details>
<summary><code>maps.DeleteFunc(m, fn)</code></summary> <summary><code>maps.DeleteFunc(m, fn)</code></summary>
A few function can fill this gap: We should implement a new receiver for the table:
```go ```go
func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) { func (t *Table[K, V]) DeleteFunc(del func(K, V) bool)
for k, v := range t.Entries() {
if del(k, v) {
t.Drop(k)
}
}
}
``` ```
It would have the same functionality as `maps.DeleteFunc`. It would have the same functionality as `maps.DeleteFunc`.
@@ -452,15 +423,22 @@ The word `Delete` is also convention, tying back to the built-in `delete()`.
<details> <details>
<summary><code>m := maps.Collect(seq)</code></summary> <summary><code>m := maps.Collect(seq)</code></summary>
This functionality would benefit from a new constructor. We should implement a new constructor.
Luckily, `t.Insert` makes this easy:
```go ```go
func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) { func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error)
t := New[K, V]() ```
err := t.Insert(seq)
return t, err It would create a `New()` table, and insert all entries in `seq`.
}
This reveicer only supports the standard table constructor, with comparable keys.
It is tempting to add `CollectBy` or `CollectCustom` to support all table types, but doing so would pollute the public interface.
It would be just one more line to initialize the table and then call `t.Insert` directly:
```go
t := // ...
err := t.Insert(seq)
``` ```
</details> </details>
@@ -468,10 +446,97 @@ func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) {
<details> <details>
<summary><code>m := map[K]V{...}</code></summary> <summary><code>m := map[K]V{...}</code></summary>
This functionality is complicated, because entries are generic; their addition cannot be through table options. We should make a new constructor, because entries are generic.
A new constructor must support this functionality. So, creating an option with inialized entries doesn't work.
Should it support options or custom hashes or `keyFunc`'s? With the previous additions, users have a few options.
No, because If they want to use a `New()` table, `t.Collect` matches well:
```go
t, err := cuckoo.Collect(func(yield func(K, V) bool) {
yield(key1, val1)
yield(key2, val2)
})
```
For `NewCustom()` or `NewBy()` tables, users can call `t.Insert` after initialization:
```go
t := // ...
err := t.Insert(func(yield func(K, V) bool) {
yield(key1, val1)
yield(key2, val2)
})
```
It is one more line.
But, the alternative is polluting the public interface with corresponding `*WithEntries` constuctors.
</details>
<details>
<summary><code>m := make(map[K]V, hint)</code></summary>
We should add a new option:
```go
func ExpectedSize(n int) Option
```
When fed to a table, it will allocate enough space to hold `n` entries without a resize.
</details>
<details>
<summary><code>clear(m)</code></summary>
We should implement a new receiver:
```go
func (t *Table[K, V]) Clear()
```
It will remove all entries from the table.
</details>
<details>
<summary><code>m2 := maps.Clone(m)</code></summary>
We should implement a matching function:
```go
func (t *Table[K, V]) Clone() *Table[K, V]
```
Also, it will copy the hash, equality function, and options used in the table.
</details>
<details>
<summary><code>it := maps.Keys(m)</code></summary>
We should implement a matching function:
```go
func (t *Table[K, V]) Keys() iter.Seq[K]
```
It is tempting to just have `All()`, but it returns a `Seq2`, not a `Seq`.
There is no iterator adaptor between `Seq` and `Seq2`, and will not be for the foreseeable future.
This function, while it feels superfluous, is required.
</details>
<details>
<summary><code>it := maps.Values(m)</code></summary>
We should implement a matching function:
```go
func (t *Table[K, V]) Values() iter.Seq[V]
```
For the same reason we need `Keys()`, we also need `Values()`.
</details> </details>