From 4464af781a01a044aa74b39fe8c71dd1bb492325 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Wed, 29 Apr 2026 20:52:15 -0400 Subject: [PATCH 01/19] feat: current contract list, started similarity --- adr/001_interface_design.md | 217 ++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 adr/001_interface_design.md diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md new file mode 100644 index 0000000..7afe20a --- /dev/null +++ b/adr/001_interface_design.md @@ -0,0 +1,217 @@ +# Designing an Idiomatic Interface + +Currently, the contract for package was built without design. +More attention was paid to implementing the underlying functionality of the cuckoo hashing. + +With the fundamentals of the algorithm built, our API contract should be revisited. +It should align closer to the following principles: + +- **Similarity to the builtin map.** + If our cuckoo table behaves similarly to Go's standard map, our user will intuitively know how to use it. + This lowers the cognitive load our developers must carry. + +## Current State + +### Interface of the Builtin Map + +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. + +
+Interfaces + +| # | Builtin Interface | Description | +| --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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. | +| 3 | `m := map[K]V{...}` | Returns a map, which may be filled with entries in the ellipsis (optional). | +| 4 | `var m map[K]V` | Defines an empty _variable_ that holds a map. This differs from #1 because `m` is uninitialized (nil) here. | +| 5 | `m[k] := v` | Assigns the value of `k` to `v`. | +| 6 | `v := m[k]` | Returns the value of `k` if it exists. Otherwise, `v` is uninitialized. | +| 7 | `v, ok := m[k]` | Similar to #6, except `ok` is equal to whether `v` is initialized. This is comma-ok notation. | +| 8 | `for k, v := range m` | Iterates over every key-value pair in `m`. The order is random. | +| 9 | `delete(m, k)` | Unassigns the value `k`. Returns no value. | +| 10 | `clear(m)` | Unassigns all keys in `m`. Returns no value. | +| 11 | `n := len(m)` | Returns the number of entries in `m`. If nil, `m` returns 0. | +| 12 | `m2 := maps.Clone(m)` | Returns a copy of `m`. | +| 13 | `maps.Copy(dst, src)` | Assigns every entry of `src` in `dst`. | +| 14 | `ok := maps.Equal(m1, m2)` | Returns true iff `m1` and `m2` the same entries. | +| 15 | `ok := maps.EqualFunc(m1, m2, fn)` | Like #14, but with a custom comparator for non-comparable values. | +| 16 | `maps.DeleteFunc(m, fn)` | Removes every entry in `m` which satisfies `fn`. Returns no value. | +| 17 | `it2 := maps.All(m)` | Returns an 2D iterator over every key-value pair. | +| 18 | `it := maps.Keys(m)` | Returns an iterator over every key. | +| 19 | `it := maps.Values(m)` | Returns an iterator over every value. There can be duplicates. | +| 20 | `m := maps.Collect(seq)` | Returns a map, with every entry defined in a 2D iterator over key-value pairs. | +| 21 | `maps.Insert(m, seq)` | Assigns to `m` all key-value pairs in 2D iterator `seq`. Returns no value. | + +
+ +### Interface of `go-cuckoo` + +On the other hand, here is the current contract for `go-cuckoo`. + +
+Interfaces + +| # | Builtin 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. | +| 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | +| 3 | `m := NewCustom(hashA, hashB, equalFunc, opts...)` | Like #1, but allows control over the hashes used to allow any key type. An `equalFunc` determines key equality. | +| 4 | `seq := m.Entries()` | Returns an unordered 2D iterator of all key-value pairs in the table. | +| 5 | `v := m.Find(k)` | Removes the value for `k`. Returns true if `k` existed. | +| 6 | `v, ok := m.Get(k)` | Returns the value for `k` in the table. Also, returns true if the `k` exists, otherwise false. When false, `v` is undefined. | +| 7 | `ok := m.Has(k)` | Returns true if `k` is in the table. | +| 8 | `err := m.Put(k, v)` | Sets value `v` for key `k`. Otherwise, returns error. | +| 9 | `n := m.Size()` | Returns the number of items in `m`. | +| 10 | `str := m.String()` | Returns `m` as a string in the format "table[k1:v1 k2:v2 ...]". | +| 11 | `cap := m.TotalCapacity()` | Returns how many slots `m` has allocated. | +| 12 | `ok := m.Drop(k)` | Removes `k` from the table. Returns whether the key had existed. | + +
+ +### Determining Similarity + +So, how do the two relate? +Listed below is an analysis of every built-in interface. +Each is compared against what `go-cuckoo` offers, and if any changes seem necessary. + +Specifically, here we are checking for functionality. +Is there functionality that this offers which `go-cuckoo` does not? +This check will check accessibility, but not discoverability. +The latter will be considered later. + +
+m := make(map[K]V) + +The analog is `m := New()`. + +
+ +
+m := make(map[K]V, hint) + +This has no analog. + +It is close to `m := New(Capacity(hint))`, but it assigns starting capacity, not expected size. +For the built-in map, these are two separate things. + +- Capacity is an internal measure, used to optimize space/speed. + It is hidden from the user because it depends on the underlying implementation, which may change. +- Expected size requires the map must hold a number of items before resizing. + This is tangeable and agnostic to implementation, hence why it is given to the user. + +In short, this interface defines expected size, but `Capacity()` defines capacity. + +
+ +
+m := map[K]V{...} + +This has no simple analog. +The closest is: + +```go +m := New[K, V]() +for k, v := range startingEntries { + m.Put(k, v) +} +``` + +While it is idiomatic, it is far less ergonomic. + +
+ +
+var m map[K]V + +The analog is `var m Table[K, V]`. + +
+ +
+m[k] := v + +
+ +
+v := m[k] + +
+ +
+v, ok := m[k] + +
+ +
+for k, v := range m + +
+ +
+delete(m, k) + +
+ +
+clear(m) + +
+ +
+n := len(m) + +
+ +
+m2 := maps.Clone(m) + +
+ +
+maps.Copy(dst, src) + +
+ +
+ok := maps.Equal(m1, m2) + +
+ +
+ok := maps.EqualFunc(m1, m2, fn) + +
+ +
+maps.DeleteFunc(m, fn) + +
+ +
+it2 := maps.All(m) + +
+ +
+it := maps.Keys(m) + +
+ +
+it := maps.Values(m) + +
+ +
+m := maps.Collect(seq) + +
+ +
+maps.Insert(m, seq) + +
+ +## Target State -- 2.52.0 From f18d48a3c25c3fbeb903f38f05039f8865ec6417 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Wed, 29 Apr 2026 20:53:07 -0400 Subject: [PATCH 02/19] fix: wording --- adr/001_interface_design.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index 7afe20a..3004d5a 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -108,8 +108,7 @@ In short, this interface defines expected size, but `Capacity()` defines capacit
m := map[K]V{...} -This has no simple analog. -The closest is: +This has no simple analog, the closest being: ```go m := New[K, V]() @@ -118,7 +117,7 @@ for k, v := range startingEntries { } ``` -While it is idiomatic, it is far less ergonomic. +It is idiomatic, but far less ergonomic.
-- 2.52.0 From a72146ca9ce0eec46d6608167e3283c17ba9aa05 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 1 May 2026 17:38:52 -0400 Subject: [PATCH 03/19] docs: finished congruency, started target state --- adr/001_interface_design.md | 246 ++++++++++++++++++++++++++++-------- 1 file changed, 194 insertions(+), 52 deletions(-) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index 3004d5a..6568244 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -6,7 +6,10 @@ More attention was paid to implementing the underlying functionality of the cuck With the fundamentals of the algorithm built, our API contract should be revisited. It should align closer to the following principles: -- **Similarity to the builtin map.** +- **Congruency to the builtin map.** + Our cuckoo table should have the same core functionality as Go's built-in map. + +- **Familiarity to the builtin map.** If our cuckoo table behaves similarly to Go's standard map, our user will intuitively know how to use it. This lowers the cognitive load our developers must carry. @@ -70,15 +73,19 @@ On the other hand, here is the current contract for `go-cuckoo`. -### Determining Similarity +### Determining Congruency -So, how do the two relate? -Listed below is an analysis of every built-in interface. -Each is compared against what `go-cuckoo` offers, and if any changes seem necessary. +So, how does the core functionality compare? +Listed below is an analysis of every interface in Go's standard map. +Each is compared against what `go-cuckoo` offers, and categorized into the following groups: + +- ✅ Covered: an analog exists. +- ⚠️ Partial: workaround available. +- ❌ Gap: no analog yet; addressed in [Target State](#solving-congruency). Specifically, here we are checking for functionality. Is there functionality that this offers which `go-cuckoo` does not? -This check will check accessibility, but not discoverability. +We are checking accessibility, but not discoverability. The latter will be considered later.
@@ -89,9 +96,9 @@ The analog is `m := New()`.
-m := make(map[K]V, hint) +⚠️ m := make(map[K]V, hint) -This has no analog. +This has no simple analog. It is close to `m := New(Capacity(hint))`, but it assigns starting capacity, not expected size. For the built-in map, these are two separate things. @@ -129,88 +136,223 @@ The analog is `var m Table[K, V]`.
-m[k] := v +m[k] := v + +The analog is `err := m.Put(k, v)`.
-v := m[k] +v := m[k] + +The analog is `v := m.Find(k)`.
-v, ok := m[k] +v, ok := m[k] + +The analog is `v, ok := m.Get(k)`.
-for k, v := range m +for k, v := range m + +The analog is `for k, v := range m.Entries()`.
-delete(m, k) +delete(m, k) + +The analog is `ok := m.Drop(k)`.
-clear(m) +clear(m) + +There is no analog. + +The easiest may to do this is to delete all items individually: + +```go +for k := range m.Entries() { + m.Drop(k) +} +```
-n := len(m) +n := len(m) + +The analog is `n := m.Size()`.
-m2 := maps.Clone(m) +m2 := maps.Clone(m) + +There is no analog. + +The easiest way to do this currently is to make a new map, and manually add the items. + +```go +m2 := cuckoo.Table[K, V]() + +for k, v := range m.Entries() { + m2.Put(k, v) +} +``` + +This gets complicated by the various options available to the user. +Furthermore, any custom `EqualFunc`, `keyFunc` or `Hash` is not transferred.
-maps.Copy(dst, src) +maps.Copy(dst, src) + +There is no analog. + +The simplest way to do this is with a for-loop. + +```go +for k, v := range src.Entries() { + dst.Put(k, v) +} +``` + +
+ +
+ok := maps.Equal(m1, m2) + +There is no analog. + +Users have to manually check the key-value pairs to determine equality. + +
+ +
+ok := maps.EqualFunc(m1, m2, fn) + +There is no analog. + +Users have to manually check the key-value pairs to determine equality. + +
+ +
+maps.DeleteFunc(m, fn) + +There is no analog. + +Users have to manually delete keys. + +
+ +
+it2 := maps.All(m) + +The analog is `it2 := m.Entries()`. + +
+ +
+⚠️ it := maps.Keys(m) + +There is no simple analog. + +A close neighbor is `it2 := m.Entries()`. +Users can use this in a for-loop, and pick out just the keys: + +```go +for k := range m.Entries() { + // ... +} +``` + +
+ +
+⚠️ it := maps.Values(m) + +There is no simple analog. + +A close neighbor is `it2 := m.Entries()`. +Users can use this in a for-loop, and pick out just the values: + +```go +for _, v := range m.Entries() { + // ... +} +``` + +
+ +
+m := maps.Collect(seq) + +There is no analog. + +
+ +
+maps.Insert(m, seq) + +There is no analog. + +
+ +## Target State + +### Solving Congruency + +The following changes will be made to accomodate for congruency: + +
+ok := maps.EqualFunc(m1, m2, fn) + +To solve this, we need a new function: + +```go +func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool +``` + +This function is free, and not bound as a receiver function. +(It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) +The latter implies `t1` has authority, when in fact neither do. + +Equality will be defined as: + +1. Neither table has a key the other doesn't. +2. Each key has the same value in each table. + Parameter `eq` determines this equality. + +Custom `EqualFunc`'s complicate this, as they modulate key identity in tables. +If two tables may differ on whether two keys are different, this function might break. +So, we must assume that: + +- Both tables have `EqualFunc`'s which 'agree' on the identity of the keys present in the tables. + Agreement is defined as: if two keys are distinct in one table, they are distinct in the other.
ok := maps.Equal(m1, m2) -
+The addition of `cuckoo.EqualFunc` makes an implementation trivial: -
-ok := maps.EqualFunc(m1, m2, fn) +```go +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. +Once again, the function is free because it is symmetric.
- -
-maps.DeleteFunc(m, fn) - -
- -
-it2 := maps.All(m) - -
- -
-it := maps.Keys(m) - -
- -
-it := maps.Values(m) - -
- -
-m := maps.Collect(seq) - -
- -
-maps.Insert(m, seq) - -
- -## Target State -- 2.52.0 From cddc205fe8baf6f91039e3d2a2971762825d27e4 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 2 May 2026 11:43:08 -0400 Subject: [PATCH 04/19] docs: insert, copy --- adr/001_interface_design.md | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index 6568244..6aba8f2 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -356,3 +356,42 @@ To conform with the standard library, a new function should be added. Once again, the function is free because it is symmetric. + +
+maps.Insert(m, seq) + +This functionality requires a new receiver: + +```go +func (t *Table[K, V]) Insert(seq *iter.Seq2[K, V]) error +``` + +A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric. +Map `dst` receives entries from map `src`. +It is only free because Go's standard map is built into the language, and so cannot have receivers. + +In terms of naming, `t.Extend` is more accurate, and has precedent in [Python](docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [Rust](https://doc.rust-lang.org/std/iter/trait.Extend.html). +Ultimately, `t.Insert()` is a better choice because of + +
+ +
+maps.Copy(dst, src) + +To solve this, we must implement a new receiver: + +```go +func (t *Table[K, V]) Copy(src *Table[K, V]) error +``` + +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. + +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()`. + In this case, `t.Merge()` would be an outlier. +- `t.Merge()` implies some sort of conflict-resolution, when there is not. + It simply overwrites the values. + +
-- 2.52.0 From 5c84ed7794d6fff0d075eb4d0da8231e3cb2d39e Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Mon, 4 May 2026 19:16:42 -0400 Subject: [PATCH 05/19] docs: DeleteFunc, Collect --- adr/001_interface_design.md | 92 ++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index 6aba8f2..0ba381c 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -319,7 +319,19 @@ The following changes will be made to accomodate for congruency: To solve this, we need a new function: ```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. @@ -339,6 +351,10 @@ So, we must assume that: - Both tables have `EqualFunc`'s which 'agree' on the identity of the keys present in the tables. Agreement is defined as: if two keys are distinct in one table, they are distinct in the other. +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. +The documentation attached to it would be moved to `DefaultEqualFunc`. +
@@ -363,25 +379,38 @@ Once again, the function is free because it is symmetric. This functionality requires a new receiver: ```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. Map `dst` receives entries from map `src`. -It is only free because Go's standard map is built into the language, and so cannot have receivers. +It's only free because Go's standard map is built into the language, and so cannot have receivers. In terms of naming, `t.Extend` is more accurate, and has precedent in [Python](docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [Rust](https://doc.rust-lang.org/std/iter/trait.Extend.html). -Ultimately, `t.Insert()` is a better choice because of +When [adding iterator function](https://github.com/golang/go/issues/61900) to the `maps` package, the Go team chose to frame it as 'sources' and 'sinks'. +With this model, `maps.Insert` made more sense than `maps.Extend`. +Ultimately, `t.Insert()` is a better choice to be consistent with `maps`.
maps.Copy(dst, src) -To solve this, we must implement a new receiver: +To solve this, we must implement a new receiver. +Luckily, `t.Insert` makes it trivial: ```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()) +} ``` A receiver fits better even though `maps.Copy` is a free function, 'copying' it is asymmetric: `dst` is writen into by `src`. @@ -395,3 +424,54 @@ The name `t.Merge()` might be more accurate, but it does work because: It simply overwrites the values.
+ +
+maps.DeleteFunc(m, fn) + +A few function can fill this gap: + +```go +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`. + +A free function could work here, but `t` has clear authority over `del`. +Other than being consistent with the `maps` package, `t.DeleteFunc` follows the Go convention of appending `Func` to higher-order equivalents of functions. +This trumps names like `t.DeleteIf`, which lend more to [Java](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#removeIf-java.util.function.Predicate-) or [C++](https://en.cppreference.com/cpp/algorithm/remove). +The word `Delete` is also convention, tying back to the built-in `delete()`. + +
+ +
+m := maps.Collect(seq) + +This functionality would benefit from a new constructor. +Luckily, `t.Insert` makes this easy: + +```go +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 +} +``` + +
+ +
+m := map[K]V{...} + +This functionality is complicated, because entries are generic; their addition cannot be through table options. +A new constructor must support this functionality. + +Should it support options or custom hashes or `keyFunc`'s? +No, because + +
-- 2.52.0 From bd25bb69bd7586ce8b39e51d0a65a6893549acf6 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 9 May 2026 16:57:58 -0400 Subject: [PATCH 06/19] docs: congruency target --- adr/001_interface_design.md | 203 ++++++++++++++++++++++++------------ 1 file changed, 134 insertions(+), 69 deletions(-) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index 0ba381c..b75df85 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -1,21 +1,21 @@ -# Designing an Idiomatic Interface +# Designing an Idiomatic API Interface -Currently, the contract for package was built without design. -More attention was paid to implementing the underlying functionality of the cuckoo hashing. +We (the maintainers) built `go-cuckoo`'s API interface without design intent. +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: -- **Congruency to the builtin map.** - Our cuckoo table should have the same core functionality as Go's built-in map. +- **Congruency** + A `go-cuckoo` table should have the same core functionality as Go's built-in map. -- **Familiarity to the builtin map.** - If our cuckoo table behaves similarly to Go's standard map, our user will intuitively know how to use it. - This lowers the cognitive load our developers must carry. +- **Familiarity** + A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. + In effect, its users will carry less cognitive load. ## 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. 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
Interfaces -| # | Builtin Interface | Description | +| # | built-in Interface | Description | | --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 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. | @@ -56,7 +56,7 @@ On the other hand, here is the current contract for `go-cuckoo`.
Interfaces -| # | 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. | | 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 -The following changes will be made to accomodate for congruency: +We should make the following changes to accomodate for congruency:
ok := maps.EqualFunc(m1, m2, fn) -To solve this, we need a new function: +We should implement a new function: ```go -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 -} +func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool ``` This function is free, and not bound as a receiver function. (It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) 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. 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`. 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`.
ok := maps.Equal(m1, m2) -The addition of `cuckoo.EqualFunc` makes an implementation trivial: +We should implement a new function, to conform with the standard library: ```go -func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool { - return EqualFunc(t1, t2, DefaultEqualFunc[V]) -} +func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool ``` -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.
@@ -376,18 +362,10 @@ Once again, the function is free because it is symmetric.
maps.Insert(m, seq) -This functionality requires a new receiver: +We should implement a new receiver for the table: ```go -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 -} +func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error ``` 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`.
maps.Copy(dst, src) -To solve this, we must implement a new receiver. -Luckily, `t.Insert` makes it trivial: +We should implement a new receiver for the table: ```go -func (t *Table[K, V]) Copy(src *Table[K, V]) error { - return t.Insert(src.Entries()) -} +func (t *Table[K, V]) Copy(src *Table[K, V]) error ``` +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`. 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: -- `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. - `t.Merge()` implies some sort of conflict-resolution, when there is not. It simply overwrites the values. @@ -428,16 +405,10 @@ The name `t.Merge()` might be more accurate, but it does work because:
maps.DeleteFunc(m, fn) -A few function can fill this gap: +We should implement a new receiver for the table: ```go -func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) { - for k, v := range t.Entries() { - if del(k, v) { - t.Drop(k) - } - } -} +func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) ``` 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()`.
m := maps.Collect(seq) -This functionality would benefit from a new constructor. -Luckily, `t.Insert` makes this easy: +We should implement a new constructor. ```go -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 -} +func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) +``` + +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) ```
@@ -468,10 +446,97 @@ func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) {
m := map[K]V{...} -This functionality is complicated, because entries are generic; their addition cannot be through table options. -A new constructor must support this functionality. +We should make a new constructor, because entries are generic. +So, creating an option with inialized entries doesn't work. -Should it support options or custom hashes or `keyFunc`'s? -No, because +With the previous additions, users have a few options. +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. + +
+ +
+m := make(map[K]V, hint) + +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. + +
+ +
+clear(m) + +We should implement a new receiver: + +```go +func (t *Table[K, V]) Clear() +``` + +It will remove all entries from the table. + +
+ +
+m2 := maps.Clone(m) + +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. + +
+ +
+it := maps.Keys(m) + +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. + +
+ +
+it := maps.Values(m) + +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()`.
-- 2.52.0 From 3aa5be87f24f2c0cc269946a6a6287c359821a2e Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 16 May 2026 14:28:33 -0400 Subject: [PATCH 07/19] feat: progress up to this point --- adr/001_interface_design.md | 107 +++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md index b75df85..28a2bf5 100644 --- a/adr/001_interface_design.md +++ b/adr/001_interface_design.md @@ -1,5 +1,14 @@ # Designing an Idiomatic API Interface +- [Designing an Idiomatic API Interface](#designing-an-idiomatic-api-interface) + - [Current State](#current-state) + - [Interface of the Built-in Map](#interface-of-the-built-in-map) + - [Interface of `go-cuckoo`](#interface-of-go-cuckoo) + - [Determining Congruency](#determining-congruency) + - [Determining Familiarity](#determining-familiarity) + - [Target State](#target-state) + - [Solving Congruency](#solving-congruency) + We (the maintainers) built `go-cuckoo`'s API interface without design intent. Up until now, we paid more attention implementing the underlying functionality of the cuckoo hashing. @@ -15,7 +24,7 @@ It should align closer to the following principles: ## Current State -### Interface of the built-in Map +### Interface of the Built-in Map 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. @@ -23,7 +32,7 @@ Also included, are the functions from the package `maps` in the standard library
Interfaces -| # | built-in Interface | Description | +| # | Built-in Interface | Description | | --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 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. | @@ -56,7 +65,7 @@ On the other hand, here is the current contract for `go-cuckoo`.
Interfaces -| # | built-in Interface | Description | +| # | `go-cuckoo` 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. | | 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | @@ -307,6 +316,98 @@ There is no analog.
+### Determining Familiarity + +We can categorize all existing table functionality by each interface's familiarity: + +- ✅ Idiomatic: is clear, intuitive, and easily understood. +- ❌ Non-idiomatic: is misleading; addressed in [Target State](#solving-congruency). + +
+m := New(opts...) + +Criteria: + +Noun/adjective form — Go constructors use NewX where X is a noun or adjective, not a verb or past participle (NewReaderSize, not NewSized) +Names what the user provides — the suffix should hint at the distinguishing parameter (NewBufferString tells you it takes a string) +Progression is readable — the three names together should imply simple → intermediate → advanced +Not misleadingly generic — NewWith or NewConfig could mean anything + +
+ +
+m := NewBy(keyFunc, opts...) + +- Use `NewKeyed()`. + +
+ +
+m := NewCustom(hashA, hashB, equalFunc, opts...) + +- Use `NewHashed()`. + +
+ +
+seq := m.Entries() + +- Call it `All()`. + +
+ +
+v := m.Find(k) + +- Call it `m.Lookup()`. The name `m.Find` implies a search algorithm. + +
+ +
+v, ok := m.Get(k) + +
+ +
+ok := m.Has(k) + +
+ +
+err := m.Put(k, v) + +- Call it `Set()`. +- No built-in library consensus, but 3rd party packages prefer `Set()`. + +
+ +
+n := m.Size() + +- Call it `Len()`. +- Size is a Java idiom. + +
+ +
+str := m.String() + +
+ +
+cap := m.TotalCapacity() + +- Remove. This is an implementation detail. + +
+ +
+ok := m.Drop(k) + +- Call it `Delete()`. + +
+ ## Target State ### Solving Congruency -- 2.52.0 From 56096bd83f5fb6b919cb25d59df8aa73ce38c50c Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:05:36 -0400 Subject: [PATCH 08/19] feat: adr for design principles, template --- adr/000_template.md | 15 + adr/001_design_principles.md | 42 +++ adr/001_interface_design.md | 643 ----------------------------------- 3 files changed, 57 insertions(+), 643 deletions(-) create mode 100644 adr/000_template.md create mode 100644 adr/001_design_principles.md delete mode 100644 adr/001_interface_design.md diff --git a/adr/000_template.md b/adr/000_template.md new file mode 100644 index 0000000..eb36d72 --- /dev/null +++ b/adr/000_template.md @@ -0,0 +1,15 @@ +# 000: {{TITLE}} + +**Status**: + +## Context + + + +## Decision + + + +## Consequences + + diff --git a/adr/001_design_principles.md b/adr/001_design_principles.md new file mode 100644 index 0000000..f7c4de5 --- /dev/null +++ b/adr/001_design_principles.md @@ -0,0 +1,42 @@ +# Adopt Congruent and Familiar Design For `go-cuckoo` + +**Status**: Proposed + +## Context + +I built `go-cuckoo`'s API interface without design intent. +Up until now, I paid more attention implementing the underlying functionality of the cuckoo hashing. +With the fundamentals of the algorithm built, I should revisit the interface. + +The goal of this project was to create an implementation of cuckoo hashing, while adhering to Go's idioms, and being as usable as possible. +While the implementation does work, it lacks direction. + +## Decision + +To resolve this, I'm enforcing two new principles onto the contract of `go-cuckoo`: + +- **Congruency**: + A `go-cuckoo` table should have the same core functionality as Go's built-in map. + +- **Familiarity**: + A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. + In effect, its users will carry less cognitive load. + +These principles should _guide_ the public interface of `go-cuckoo`. +Neither should be treated absolutely, though. +The behavior of `go-cuckoo` is distinct from `map`. +Do not equate them. + +## Consequences + +1. The repository should support both design principles. + - The `README.md` and `doc.go` should reflect these principles. + - The contributing guide and pull request template should require these principles. +2. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. + - Its construction should uncover any current incongruencies in the interfaces. + - I should prioritize limiting any incongruencies. + - The document should be visible from the `README.md`. +3. An analysis of the familiarity of `go-cuckoo`'s interface should be made. + - Unlike the analysis of congruency, this should be a one time document. + Familiarity is implicit to users, and does not need to be referenced. + But, any rationale should be documented in commit messages, or future ADRs. diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md deleted file mode 100644 index 28a2bf5..0000000 --- a/adr/001_interface_design.md +++ /dev/null @@ -1,643 +0,0 @@ -# Designing an Idiomatic API Interface - -- [Designing an Idiomatic API Interface](#designing-an-idiomatic-api-interface) - - [Current State](#current-state) - - [Interface of the Built-in Map](#interface-of-the-built-in-map) - - [Interface of `go-cuckoo`](#interface-of-go-cuckoo) - - [Determining Congruency](#determining-congruency) - - [Determining Familiarity](#determining-familiarity) - - [Target State](#target-state) - - [Solving Congruency](#solving-congruency) - -We (the maintainers) built `go-cuckoo`'s API interface without design intent. -Up until now, we paid more attention implementing the underlying functionality of the cuckoo hashing. - -With the fundamentals of the algorithm built, we should revisit the interface. -It should align closer to the following principles: - -- **Congruency** - A `go-cuckoo` table should have the same core functionality as Go's built-in map. - -- **Familiarity** - A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. - In effect, its users will carry less cognitive load. - -## Current State - -### Interface of the Built-in Map - -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. - -
-Interfaces - -| # | Built-in Interface | Description | -| --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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. | -| 3 | `m := map[K]V{...}` | Returns a map, which may be filled with entries in the ellipsis (optional). | -| 4 | `var m map[K]V` | Defines an empty _variable_ that holds a map. This differs from #1 because `m` is uninitialized (nil) here. | -| 5 | `m[k] := v` | Assigns the value of `k` to `v`. | -| 6 | `v := m[k]` | Returns the value of `k` if it exists. Otherwise, `v` is uninitialized. | -| 7 | `v, ok := m[k]` | Similar to #6, except `ok` is equal to whether `v` is initialized. This is comma-ok notation. | -| 8 | `for k, v := range m` | Iterates over every key-value pair in `m`. The order is random. | -| 9 | `delete(m, k)` | Unassigns the value `k`. Returns no value. | -| 10 | `clear(m)` | Unassigns all keys in `m`. Returns no value. | -| 11 | `n := len(m)` | Returns the number of entries in `m`. If nil, `m` returns 0. | -| 12 | `m2 := maps.Clone(m)` | Returns a copy of `m`. | -| 13 | `maps.Copy(dst, src)` | Assigns every entry of `src` in `dst`. | -| 14 | `ok := maps.Equal(m1, m2)` | Returns true iff `m1` and `m2` the same entries. | -| 15 | `ok := maps.EqualFunc(m1, m2, fn)` | Like #14, but with a custom comparator for non-comparable values. | -| 16 | `maps.DeleteFunc(m, fn)` | Removes every entry in `m` which satisfies `fn`. Returns no value. | -| 17 | `it2 := maps.All(m)` | Returns an 2D iterator over every key-value pair. | -| 18 | `it := maps.Keys(m)` | Returns an iterator over every key. | -| 19 | `it := maps.Values(m)` | Returns an iterator over every value. There can be duplicates. | -| 20 | `m := maps.Collect(seq)` | Returns a map, with every entry defined in a 2D iterator over key-value pairs. | -| 21 | `maps.Insert(m, seq)` | Assigns to `m` all key-value pairs in 2D iterator `seq`. Returns no value. | - -
- -### Interface of `go-cuckoo` - -On the other hand, here is the current contract for `go-cuckoo`. - -
-Interfaces - -| # | `go-cuckoo` 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. | -| 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | -| 3 | `m := NewCustom(hashA, hashB, equalFunc, opts...)` | Like #1, but allows control over the hashes used to allow any key type. An `equalFunc` determines key equality. | -| 4 | `seq := m.Entries()` | Returns an unordered 2D iterator of all key-value pairs in the table. | -| 5 | `v := m.Find(k)` | Removes the value for `k`. Returns true if `k` existed. | -| 6 | `v, ok := m.Get(k)` | Returns the value for `k` in the table. Also, returns true if the `k` exists, otherwise false. When false, `v` is undefined. | -| 7 | `ok := m.Has(k)` | Returns true if `k` is in the table. | -| 8 | `err := m.Put(k, v)` | Sets value `v` for key `k`. Otherwise, returns error. | -| 9 | `n := m.Size()` | Returns the number of items in `m`. | -| 10 | `str := m.String()` | Returns `m` as a string in the format "table[k1:v1 k2:v2 ...]". | -| 11 | `cap := m.TotalCapacity()` | Returns how many slots `m` has allocated. | -| 12 | `ok := m.Drop(k)` | Removes `k` from the table. Returns whether the key had existed. | - -
- -### Determining Congruency - -So, how does the core functionality compare? -Listed below is an analysis of every interface in Go's standard map. -Each is compared against what `go-cuckoo` offers, and categorized into the following groups: - -- ✅ Covered: an analog exists. -- ⚠️ Partial: workaround available. -- ❌ Gap: no analog yet; addressed in [Target State](#solving-congruency). - -Specifically, here we are checking for functionality. -Is there functionality that this offers which `go-cuckoo` does not? -We are checking accessibility, but not discoverability. -The latter will be considered later. - -
-m := make(map[K]V) - -The analog is `m := New()`. - -
- -
-⚠️ m := make(map[K]V, hint) - -This has no simple analog. - -It is close to `m := New(Capacity(hint))`, but it assigns starting capacity, not expected size. -For the built-in map, these are two separate things. - -- Capacity is an internal measure, used to optimize space/speed. - It is hidden from the user because it depends on the underlying implementation, which may change. -- Expected size requires the map must hold a number of items before resizing. - This is tangeable and agnostic to implementation, hence why it is given to the user. - -In short, this interface defines expected size, but `Capacity()` defines capacity. - -
- -
-m := map[K]V{...} - -This has no simple analog, the closest being: - -```go -m := New[K, V]() -for k, v := range startingEntries { - m.Put(k, v) -} -``` - -It is idiomatic, but far less ergonomic. - -
- -
-var m map[K]V - -The analog is `var m Table[K, V]`. - -
- -
-m[k] := v - -The analog is `err := m.Put(k, v)`. - -
- -
-v := m[k] - -The analog is `v := m.Find(k)`. - -
- -
-v, ok := m[k] - -The analog is `v, ok := m.Get(k)`. - -
- -
-for k, v := range m - -The analog is `for k, v := range m.Entries()`. - -
- -
-delete(m, k) - -The analog is `ok := m.Drop(k)`. - -
- -
-clear(m) - -There is no analog. - -The easiest may to do this is to delete all items individually: - -```go -for k := range m.Entries() { - m.Drop(k) -} -``` - -
- -
-n := len(m) - -The analog is `n := m.Size()`. - -
- -
-m2 := maps.Clone(m) - -There is no analog. - -The easiest way to do this currently is to make a new map, and manually add the items. - -```go -m2 := cuckoo.Table[K, V]() - -for k, v := range m.Entries() { - m2.Put(k, v) -} -``` - -This gets complicated by the various options available to the user. -Furthermore, any custom `EqualFunc`, `keyFunc` or `Hash` is not transferred. - -
- -
-maps.Copy(dst, src) - -There is no analog. - -The simplest way to do this is with a for-loop. - -```go -for k, v := range src.Entries() { - dst.Put(k, v) -} -``` - -
- -
-ok := maps.Equal(m1, m2) - -There is no analog. - -Users have to manually check the key-value pairs to determine equality. - -
- -
-ok := maps.EqualFunc(m1, m2, fn) - -There is no analog. - -Users have to manually check the key-value pairs to determine equality. - -
- -
-maps.DeleteFunc(m, fn) - -There is no analog. - -Users have to manually delete keys. - -
- -
-it2 := maps.All(m) - -The analog is `it2 := m.Entries()`. - -
- -
-⚠️ it := maps.Keys(m) - -There is no simple analog. - -A close neighbor is `it2 := m.Entries()`. -Users can use this in a for-loop, and pick out just the keys: - -```go -for k := range m.Entries() { - // ... -} -``` - -
- -
-⚠️ it := maps.Values(m) - -There is no simple analog. - -A close neighbor is `it2 := m.Entries()`. -Users can use this in a for-loop, and pick out just the values: - -```go -for _, v := range m.Entries() { - // ... -} -``` - -
- -
-m := maps.Collect(seq) - -There is no analog. - -
- -
-maps.Insert(m, seq) - -There is no analog. - -
- -### Determining Familiarity - -We can categorize all existing table functionality by each interface's familiarity: - -- ✅ Idiomatic: is clear, intuitive, and easily understood. -- ❌ Non-idiomatic: is misleading; addressed in [Target State](#solving-congruency). - -
-m := New(opts...) - -Criteria: - -Noun/adjective form — Go constructors use NewX where X is a noun or adjective, not a verb or past participle (NewReaderSize, not NewSized) -Names what the user provides — the suffix should hint at the distinguishing parameter (NewBufferString tells you it takes a string) -Progression is readable — the three names together should imply simple → intermediate → advanced -Not misleadingly generic — NewWith or NewConfig could mean anything - -
- -
-m := NewBy(keyFunc, opts...) - -- Use `NewKeyed()`. - -
- -
-m := NewCustom(hashA, hashB, equalFunc, opts...) - -- Use `NewHashed()`. - -
- -
-seq := m.Entries() - -- Call it `All()`. - -
- -
-v := m.Find(k) - -- Call it `m.Lookup()`. The name `m.Find` implies a search algorithm. - -
- -
-v, ok := m.Get(k) - -
- -
-ok := m.Has(k) - -
- -
-err := m.Put(k, v) - -- Call it `Set()`. -- No built-in library consensus, but 3rd party packages prefer `Set()`. - -
- -
-n := m.Size() - -- Call it `Len()`. -- Size is a Java idiom. - -
- -
-str := m.String() - -
- -
-cap := m.TotalCapacity() - -- Remove. This is an implementation detail. - -
- -
-ok := m.Drop(k) - -- Call it `Delete()`. - -
- -## Target State - -### Solving Congruency - -We should make the following changes to accomodate for congruency: - -
-ok := maps.EqualFunc(m1, m2, fn) - -We should implement a new function: - -```go -func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool -``` - -This function is free, and not bound as a receiver function. -(It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) -The latter implies `t1` has authority, when in fact neither do. - -We define equality as: - -1. Neither table has a key the other doesn't. -2. Each key has the same value in each table. - Parameter `eq` determines this equality. - -Custom `EqualFunc`'s complicate this, as they modulate key identity in tables. -If two tables may differ on whether two keys are different, this function might break. -So, we must assume that: - -- Both tables have `EqualFunc`'s which 'agree' on the identity of the keys present in the tables. - Agreement is defined as: if two keys are distinct in one table, they are distinct in the other. - -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. -We will move the documentation attached to it to `DefaultEqualFunc`. - -
- -
-ok := maps.Equal(m1, m2) - -We should implement a new function, to conform with the standard library: - -```go -func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool -``` - -It uses the same equality check as in `EqualFunc`. -Once again, the function is free because it is symmetric. - -
- -
-maps.Insert(m, seq) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error -``` - -A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric. -Map `dst` receives entries from map `src`. -It's only free because Go's standard map is built into the language, and so cannot have receivers. - -In terms of naming, `t.Extend` is more accurate, and has precedent in [Python](docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [Rust](https://doc.rust-lang.org/std/iter/trait.Extend.html). -When [adding iterator function](https://github.com/golang/go/issues/61900) to the `maps` package, the Go team chose to frame it as 'sources' and 'sinks'. -With this model, `maps.Insert` made more sense than `maps.Extend`. -Ultimately, `t.Insert()` is a better choice to be consistent with `maps`. - -
- -
-maps.Copy(dst, src) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) Copy(src *Table[K, V]) error -``` - -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`. -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: - -- `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. -- `t.Merge()` implies some sort of conflict-resolution, when there is not. - It simply overwrites the values. - -
- -
-maps.DeleteFunc(m, fn) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) -``` - -It would have the same functionality as `maps.DeleteFunc`. - -A free function could work here, but `t` has clear authority over `del`. -Other than being consistent with the `maps` package, `t.DeleteFunc` follows the Go convention of appending `Func` to higher-order equivalents of functions. -This trumps names like `t.DeleteIf`, which lend more to [Java](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#removeIf-java.util.function.Predicate-) or [C++](https://en.cppreference.com/cpp/algorithm/remove). -The word `Delete` is also convention, tying back to the built-in `delete()`. - -
- -
-m := maps.Collect(seq) - -We should implement a new constructor. - -```go -func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) -``` - -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) -``` - -
- -
-m := map[K]V{...} - -We should make a new constructor, because entries are generic. -So, creating an option with inialized entries doesn't work. - -With the previous additions, users have a few options. -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. - -
- -
-m := make(map[K]V, hint) - -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. - -
- -
-clear(m) - -We should implement a new receiver: - -```go -func (t *Table[K, V]) Clear() -``` - -It will remove all entries from the table. - -
- -
-m2 := maps.Clone(m) - -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. - -
- -
-it := maps.Keys(m) - -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. - -
- -
-it := maps.Values(m) - -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()`. - -
-- 2.52.0 From 7bc42e71010a5bb314fce8b8873d51f4ea1a6ac0 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:21:50 -0400 Subject: [PATCH 09/19] style: worded consequences more as a checklist --- adr/001_design_principles.md | 20 +- adr/001_interface_design.md | 643 +++++++++++++++++++++++++++++++++++ 2 files changed, 654 insertions(+), 9 deletions(-) create mode 100644 adr/001_interface_design.md diff --git a/adr/001_design_principles.md b/adr/001_design_principles.md index f7c4de5..43dd41f 100644 --- a/adr/001_design_principles.md +++ b/adr/001_design_principles.md @@ -30,13 +30,15 @@ Do not equate them. ## Consequences 1. The repository should support both design principles. - - The `README.md` and `doc.go` should reflect these principles. - - The contributing guide and pull request template should require these principles. + - [ ] Update the `README.md` and `doc.go` to reflect these principles. + - [ ] Update the contributing guide and pull request template to require these principles are met. 2. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. - - Its construction should uncover any current incongruencies in the interfaces. - - I should prioritize limiting any incongruencies. - - The document should be visible from the `README.md`. -3. An analysis of the familiarity of `go-cuckoo`'s interface should be made. - - Unlike the analysis of congruency, this should be a one time document. - Familiarity is implicit to users, and does not need to be referenced. - But, any rationale should be documented in commit messages, or future ADRs. + I should prioritize limiting any incongruencies. + - [ ] Produce the first draft to uncover any current incongruencies. + - [ ] Link the document to the `README.md`. +3. Analyze the familiarity of `go-cuckoo`'s current interface. + Unlike the analysis of congruency, this should be a one time document. + Familiarity is implicit to users, and does not need to be referenced. + But, any rationale should be documented in commit messages, or future ADRs. + - [ ] Produce the analysis document. + - [ ] Resolve any issues found. diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md new file mode 100644 index 0000000..28a2bf5 --- /dev/null +++ b/adr/001_interface_design.md @@ -0,0 +1,643 @@ +# Designing an Idiomatic API Interface + +- [Designing an Idiomatic API Interface](#designing-an-idiomatic-api-interface) + - [Current State](#current-state) + - [Interface of the Built-in Map](#interface-of-the-built-in-map) + - [Interface of `go-cuckoo`](#interface-of-go-cuckoo) + - [Determining Congruency](#determining-congruency) + - [Determining Familiarity](#determining-familiarity) + - [Target State](#target-state) + - [Solving Congruency](#solving-congruency) + +We (the maintainers) built `go-cuckoo`'s API interface without design intent. +Up until now, we paid more attention implementing the underlying functionality of the cuckoo hashing. + +With the fundamentals of the algorithm built, we should revisit the interface. +It should align closer to the following principles: + +- **Congruency** + A `go-cuckoo` table should have the same core functionality as Go's built-in map. + +- **Familiarity** + A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. + In effect, its users will carry less cognitive load. + +## Current State + +### Interface of the Built-in Map + +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. + +
+Interfaces + +| # | Built-in Interface | Description | +| --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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. | +| 3 | `m := map[K]V{...}` | Returns a map, which may be filled with entries in the ellipsis (optional). | +| 4 | `var m map[K]V` | Defines an empty _variable_ that holds a map. This differs from #1 because `m` is uninitialized (nil) here. | +| 5 | `m[k] := v` | Assigns the value of `k` to `v`. | +| 6 | `v := m[k]` | Returns the value of `k` if it exists. Otherwise, `v` is uninitialized. | +| 7 | `v, ok := m[k]` | Similar to #6, except `ok` is equal to whether `v` is initialized. This is comma-ok notation. | +| 8 | `for k, v := range m` | Iterates over every key-value pair in `m`. The order is random. | +| 9 | `delete(m, k)` | Unassigns the value `k`. Returns no value. | +| 10 | `clear(m)` | Unassigns all keys in `m`. Returns no value. | +| 11 | `n := len(m)` | Returns the number of entries in `m`. If nil, `m` returns 0. | +| 12 | `m2 := maps.Clone(m)` | Returns a copy of `m`. | +| 13 | `maps.Copy(dst, src)` | Assigns every entry of `src` in `dst`. | +| 14 | `ok := maps.Equal(m1, m2)` | Returns true iff `m1` and `m2` the same entries. | +| 15 | `ok := maps.EqualFunc(m1, m2, fn)` | Like #14, but with a custom comparator for non-comparable values. | +| 16 | `maps.DeleteFunc(m, fn)` | Removes every entry in `m` which satisfies `fn`. Returns no value. | +| 17 | `it2 := maps.All(m)` | Returns an 2D iterator over every key-value pair. | +| 18 | `it := maps.Keys(m)` | Returns an iterator over every key. | +| 19 | `it := maps.Values(m)` | Returns an iterator over every value. There can be duplicates. | +| 20 | `m := maps.Collect(seq)` | Returns a map, with every entry defined in a 2D iterator over key-value pairs. | +| 21 | `maps.Insert(m, seq)` | Assigns to `m` all key-value pairs in 2D iterator `seq`. Returns no value. | + +
+ +### Interface of `go-cuckoo` + +On the other hand, here is the current contract for `go-cuckoo`. + +
+Interfaces + +| # | `go-cuckoo` 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. | +| 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | +| 3 | `m := NewCustom(hashA, hashB, equalFunc, opts...)` | Like #1, but allows control over the hashes used to allow any key type. An `equalFunc` determines key equality. | +| 4 | `seq := m.Entries()` | Returns an unordered 2D iterator of all key-value pairs in the table. | +| 5 | `v := m.Find(k)` | Removes the value for `k`. Returns true if `k` existed. | +| 6 | `v, ok := m.Get(k)` | Returns the value for `k` in the table. Also, returns true if the `k` exists, otherwise false. When false, `v` is undefined. | +| 7 | `ok := m.Has(k)` | Returns true if `k` is in the table. | +| 8 | `err := m.Put(k, v)` | Sets value `v` for key `k`. Otherwise, returns error. | +| 9 | `n := m.Size()` | Returns the number of items in `m`. | +| 10 | `str := m.String()` | Returns `m` as a string in the format "table[k1:v1 k2:v2 ...]". | +| 11 | `cap := m.TotalCapacity()` | Returns how many slots `m` has allocated. | +| 12 | `ok := m.Drop(k)` | Removes `k` from the table. Returns whether the key had existed. | + +
+ +### Determining Congruency + +So, how does the core functionality compare? +Listed below is an analysis of every interface in Go's standard map. +Each is compared against what `go-cuckoo` offers, and categorized into the following groups: + +- ✅ Covered: an analog exists. +- ⚠️ Partial: workaround available. +- ❌ Gap: no analog yet; addressed in [Target State](#solving-congruency). + +Specifically, here we are checking for functionality. +Is there functionality that this offers which `go-cuckoo` does not? +We are checking accessibility, but not discoverability. +The latter will be considered later. + +
+m := make(map[K]V) + +The analog is `m := New()`. + +
+ +
+⚠️ m := make(map[K]V, hint) + +This has no simple analog. + +It is close to `m := New(Capacity(hint))`, but it assigns starting capacity, not expected size. +For the built-in map, these are two separate things. + +- Capacity is an internal measure, used to optimize space/speed. + It is hidden from the user because it depends on the underlying implementation, which may change. +- Expected size requires the map must hold a number of items before resizing. + This is tangeable and agnostic to implementation, hence why it is given to the user. + +In short, this interface defines expected size, but `Capacity()` defines capacity. + +
+ +
+m := map[K]V{...} + +This has no simple analog, the closest being: + +```go +m := New[K, V]() +for k, v := range startingEntries { + m.Put(k, v) +} +``` + +It is idiomatic, but far less ergonomic. + +
+ +
+var m map[K]V + +The analog is `var m Table[K, V]`. + +
+ +
+m[k] := v + +The analog is `err := m.Put(k, v)`. + +
+ +
+v := m[k] + +The analog is `v := m.Find(k)`. + +
+ +
+v, ok := m[k] + +The analog is `v, ok := m.Get(k)`. + +
+ +
+for k, v := range m + +The analog is `for k, v := range m.Entries()`. + +
+ +
+delete(m, k) + +The analog is `ok := m.Drop(k)`. + +
+ +
+clear(m) + +There is no analog. + +The easiest may to do this is to delete all items individually: + +```go +for k := range m.Entries() { + m.Drop(k) +} +``` + +
+ +
+n := len(m) + +The analog is `n := m.Size()`. + +
+ +
+m2 := maps.Clone(m) + +There is no analog. + +The easiest way to do this currently is to make a new map, and manually add the items. + +```go +m2 := cuckoo.Table[K, V]() + +for k, v := range m.Entries() { + m2.Put(k, v) +} +``` + +This gets complicated by the various options available to the user. +Furthermore, any custom `EqualFunc`, `keyFunc` or `Hash` is not transferred. + +
+ +
+maps.Copy(dst, src) + +There is no analog. + +The simplest way to do this is with a for-loop. + +```go +for k, v := range src.Entries() { + dst.Put(k, v) +} +``` + +
+ +
+ok := maps.Equal(m1, m2) + +There is no analog. + +Users have to manually check the key-value pairs to determine equality. + +
+ +
+ok := maps.EqualFunc(m1, m2, fn) + +There is no analog. + +Users have to manually check the key-value pairs to determine equality. + +
+ +
+maps.DeleteFunc(m, fn) + +There is no analog. + +Users have to manually delete keys. + +
+ +
+it2 := maps.All(m) + +The analog is `it2 := m.Entries()`. + +
+ +
+⚠️ it := maps.Keys(m) + +There is no simple analog. + +A close neighbor is `it2 := m.Entries()`. +Users can use this in a for-loop, and pick out just the keys: + +```go +for k := range m.Entries() { + // ... +} +``` + +
+ +
+⚠️ it := maps.Values(m) + +There is no simple analog. + +A close neighbor is `it2 := m.Entries()`. +Users can use this in a for-loop, and pick out just the values: + +```go +for _, v := range m.Entries() { + // ... +} +``` + +
+ +
+m := maps.Collect(seq) + +There is no analog. + +
+ +
+maps.Insert(m, seq) + +There is no analog. + +
+ +### Determining Familiarity + +We can categorize all existing table functionality by each interface's familiarity: + +- ✅ Idiomatic: is clear, intuitive, and easily understood. +- ❌ Non-idiomatic: is misleading; addressed in [Target State](#solving-congruency). + +
+m := New(opts...) + +Criteria: + +Noun/adjective form — Go constructors use NewX where X is a noun or adjective, not a verb or past participle (NewReaderSize, not NewSized) +Names what the user provides — the suffix should hint at the distinguishing parameter (NewBufferString tells you it takes a string) +Progression is readable — the three names together should imply simple → intermediate → advanced +Not misleadingly generic — NewWith or NewConfig could mean anything + +
+ +
+m := NewBy(keyFunc, opts...) + +- Use `NewKeyed()`. + +
+ +
+m := NewCustom(hashA, hashB, equalFunc, opts...) + +- Use `NewHashed()`. + +
+ +
+seq := m.Entries() + +- Call it `All()`. + +
+ +
+v := m.Find(k) + +- Call it `m.Lookup()`. The name `m.Find` implies a search algorithm. + +
+ +
+v, ok := m.Get(k) + +
+ +
+ok := m.Has(k) + +
+ +
+err := m.Put(k, v) + +- Call it `Set()`. +- No built-in library consensus, but 3rd party packages prefer `Set()`. + +
+ +
+n := m.Size() + +- Call it `Len()`. +- Size is a Java idiom. + +
+ +
+str := m.String() + +
+ +
+cap := m.TotalCapacity() + +- Remove. This is an implementation detail. + +
+ +
+ok := m.Drop(k) + +- Call it `Delete()`. + +
+ +## Target State + +### Solving Congruency + +We should make the following changes to accomodate for congruency: + +
+ok := maps.EqualFunc(m1, m2, fn) + +We should implement a new function: + +```go +func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool +``` + +This function is free, and not bound as a receiver function. +(It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) +The latter implies `t1` has authority, when in fact neither do. + +We define equality as: + +1. Neither table has a key the other doesn't. +2. Each key has the same value in each table. + Parameter `eq` determines this equality. + +Custom `EqualFunc`'s complicate this, as they modulate key identity in tables. +If two tables may differ on whether two keys are different, this function might break. +So, we must assume that: + +- Both tables have `EqualFunc`'s which 'agree' on the identity of the keys present in the tables. + Agreement is defined as: if two keys are distinct in one table, they are distinct in the other. + +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. +We will move the documentation attached to it to `DefaultEqualFunc`. + +
+ +
+ok := maps.Equal(m1, m2) + +We should implement a new function, to conform with the standard library: + +```go +func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool +``` + +It uses the same equality check as in `EqualFunc`. +Once again, the function is free because it is symmetric. + +
+ +
+maps.Insert(m, seq) + +We should implement a new receiver for the table: + +```go +func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error +``` + +A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric. +Map `dst` receives entries from map `src`. +It's only free because Go's standard map is built into the language, and so cannot have receivers. + +In terms of naming, `t.Extend` is more accurate, and has precedent in [Python](docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [Rust](https://doc.rust-lang.org/std/iter/trait.Extend.html). +When [adding iterator function](https://github.com/golang/go/issues/61900) to the `maps` package, the Go team chose to frame it as 'sources' and 'sinks'. +With this model, `maps.Insert` made more sense than `maps.Extend`. +Ultimately, `t.Insert()` is a better choice to be consistent with `maps`. + +
+ +
+maps.Copy(dst, src) + +We should implement a new receiver for the table: + +```go +func (t *Table[K, V]) Copy(src *Table[K, V]) error +``` + +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`. +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: + +- `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. +- `t.Merge()` implies some sort of conflict-resolution, when there is not. + It simply overwrites the values. + +
+ +
+maps.DeleteFunc(m, fn) + +We should implement a new receiver for the table: + +```go +func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) +``` + +It would have the same functionality as `maps.DeleteFunc`. + +A free function could work here, but `t` has clear authority over `del`. +Other than being consistent with the `maps` package, `t.DeleteFunc` follows the Go convention of appending `Func` to higher-order equivalents of functions. +This trumps names like `t.DeleteIf`, which lend more to [Java](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#removeIf-java.util.function.Predicate-) or [C++](https://en.cppreference.com/cpp/algorithm/remove). +The word `Delete` is also convention, tying back to the built-in `delete()`. + +
+ +
+m := maps.Collect(seq) + +We should implement a new constructor. + +```go +func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) +``` + +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) +``` + +
+ +
+m := map[K]V{...} + +We should make a new constructor, because entries are generic. +So, creating an option with inialized entries doesn't work. + +With the previous additions, users have a few options. +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. + +
+ +
+m := make(map[K]V, hint) + +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. + +
+ +
+clear(m) + +We should implement a new receiver: + +```go +func (t *Table[K, V]) Clear() +``` + +It will remove all entries from the table. + +
+ +
+m2 := maps.Clone(m) + +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. + +
+ +
+it := maps.Keys(m) + +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. + +
+ +
+it := maps.Values(m) + +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()`. + +
-- 2.52.0 From 3f77e230a15c1e80a6358ba61e911c96d0563104 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:22:18 -0400 Subject: [PATCH 10/19] revert: no old adr --- adr/001_interface_design.md | 643 ------------------------------------ 1 file changed, 643 deletions(-) delete mode 100644 adr/001_interface_design.md diff --git a/adr/001_interface_design.md b/adr/001_interface_design.md deleted file mode 100644 index 28a2bf5..0000000 --- a/adr/001_interface_design.md +++ /dev/null @@ -1,643 +0,0 @@ -# Designing an Idiomatic API Interface - -- [Designing an Idiomatic API Interface](#designing-an-idiomatic-api-interface) - - [Current State](#current-state) - - [Interface of the Built-in Map](#interface-of-the-built-in-map) - - [Interface of `go-cuckoo`](#interface-of-go-cuckoo) - - [Determining Congruency](#determining-congruency) - - [Determining Familiarity](#determining-familiarity) - - [Target State](#target-state) - - [Solving Congruency](#solving-congruency) - -We (the maintainers) built `go-cuckoo`'s API interface without design intent. -Up until now, we paid more attention implementing the underlying functionality of the cuckoo hashing. - -With the fundamentals of the algorithm built, we should revisit the interface. -It should align closer to the following principles: - -- **Congruency** - A `go-cuckoo` table should have the same core functionality as Go's built-in map. - -- **Familiarity** - A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. - In effect, its users will carry less cognitive load. - -## Current State - -### Interface of the Built-in Map - -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. - -
-Interfaces - -| # | Built-in Interface | Description | -| --- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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. | -| 3 | `m := map[K]V{...}` | Returns a map, which may be filled with entries in the ellipsis (optional). | -| 4 | `var m map[K]V` | Defines an empty _variable_ that holds a map. This differs from #1 because `m` is uninitialized (nil) here. | -| 5 | `m[k] := v` | Assigns the value of `k` to `v`. | -| 6 | `v := m[k]` | Returns the value of `k` if it exists. Otherwise, `v` is uninitialized. | -| 7 | `v, ok := m[k]` | Similar to #6, except `ok` is equal to whether `v` is initialized. This is comma-ok notation. | -| 8 | `for k, v := range m` | Iterates over every key-value pair in `m`. The order is random. | -| 9 | `delete(m, k)` | Unassigns the value `k`. Returns no value. | -| 10 | `clear(m)` | Unassigns all keys in `m`. Returns no value. | -| 11 | `n := len(m)` | Returns the number of entries in `m`. If nil, `m` returns 0. | -| 12 | `m2 := maps.Clone(m)` | Returns a copy of `m`. | -| 13 | `maps.Copy(dst, src)` | Assigns every entry of `src` in `dst`. | -| 14 | `ok := maps.Equal(m1, m2)` | Returns true iff `m1` and `m2` the same entries. | -| 15 | `ok := maps.EqualFunc(m1, m2, fn)` | Like #14, but with a custom comparator for non-comparable values. | -| 16 | `maps.DeleteFunc(m, fn)` | Removes every entry in `m` which satisfies `fn`. Returns no value. | -| 17 | `it2 := maps.All(m)` | Returns an 2D iterator over every key-value pair. | -| 18 | `it := maps.Keys(m)` | Returns an iterator over every key. | -| 19 | `it := maps.Values(m)` | Returns an iterator over every value. There can be duplicates. | -| 20 | `m := maps.Collect(seq)` | Returns a map, with every entry defined in a 2D iterator over key-value pairs. | -| 21 | `maps.Insert(m, seq)` | Assigns to `m` all key-value pairs in 2D iterator `seq`. Returns no value. | - -
- -### Interface of `go-cuckoo` - -On the other hand, here is the current contract for `go-cuckoo`. - -
-Interfaces - -| # | `go-cuckoo` 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. | -| 2 | `m := NewBy(keyFunc, opts...)` | Like #1, but allows any key type. A `keyFunc` is used to derive a comparable key. | -| 3 | `m := NewCustom(hashA, hashB, equalFunc, opts...)` | Like #1, but allows control over the hashes used to allow any key type. An `equalFunc` determines key equality. | -| 4 | `seq := m.Entries()` | Returns an unordered 2D iterator of all key-value pairs in the table. | -| 5 | `v := m.Find(k)` | Removes the value for `k`. Returns true if `k` existed. | -| 6 | `v, ok := m.Get(k)` | Returns the value for `k` in the table. Also, returns true if the `k` exists, otherwise false. When false, `v` is undefined. | -| 7 | `ok := m.Has(k)` | Returns true if `k` is in the table. | -| 8 | `err := m.Put(k, v)` | Sets value `v` for key `k`. Otherwise, returns error. | -| 9 | `n := m.Size()` | Returns the number of items in `m`. | -| 10 | `str := m.String()` | Returns `m` as a string in the format "table[k1:v1 k2:v2 ...]". | -| 11 | `cap := m.TotalCapacity()` | Returns how many slots `m` has allocated. | -| 12 | `ok := m.Drop(k)` | Removes `k` from the table. Returns whether the key had existed. | - -
- -### Determining Congruency - -So, how does the core functionality compare? -Listed below is an analysis of every interface in Go's standard map. -Each is compared against what `go-cuckoo` offers, and categorized into the following groups: - -- ✅ Covered: an analog exists. -- ⚠️ Partial: workaround available. -- ❌ Gap: no analog yet; addressed in [Target State](#solving-congruency). - -Specifically, here we are checking for functionality. -Is there functionality that this offers which `go-cuckoo` does not? -We are checking accessibility, but not discoverability. -The latter will be considered later. - -
-m := make(map[K]V) - -The analog is `m := New()`. - -
- -
-⚠️ m := make(map[K]V, hint) - -This has no simple analog. - -It is close to `m := New(Capacity(hint))`, but it assigns starting capacity, not expected size. -For the built-in map, these are two separate things. - -- Capacity is an internal measure, used to optimize space/speed. - It is hidden from the user because it depends on the underlying implementation, which may change. -- Expected size requires the map must hold a number of items before resizing. - This is tangeable and agnostic to implementation, hence why it is given to the user. - -In short, this interface defines expected size, but `Capacity()` defines capacity. - -
- -
-m := map[K]V{...} - -This has no simple analog, the closest being: - -```go -m := New[K, V]() -for k, v := range startingEntries { - m.Put(k, v) -} -``` - -It is idiomatic, but far less ergonomic. - -
- -
-var m map[K]V - -The analog is `var m Table[K, V]`. - -
- -
-m[k] := v - -The analog is `err := m.Put(k, v)`. - -
- -
-v := m[k] - -The analog is `v := m.Find(k)`. - -
- -
-v, ok := m[k] - -The analog is `v, ok := m.Get(k)`. - -
- -
-for k, v := range m - -The analog is `for k, v := range m.Entries()`. - -
- -
-delete(m, k) - -The analog is `ok := m.Drop(k)`. - -
- -
-clear(m) - -There is no analog. - -The easiest may to do this is to delete all items individually: - -```go -for k := range m.Entries() { - m.Drop(k) -} -``` - -
- -
-n := len(m) - -The analog is `n := m.Size()`. - -
- -
-m2 := maps.Clone(m) - -There is no analog. - -The easiest way to do this currently is to make a new map, and manually add the items. - -```go -m2 := cuckoo.Table[K, V]() - -for k, v := range m.Entries() { - m2.Put(k, v) -} -``` - -This gets complicated by the various options available to the user. -Furthermore, any custom `EqualFunc`, `keyFunc` or `Hash` is not transferred. - -
- -
-maps.Copy(dst, src) - -There is no analog. - -The simplest way to do this is with a for-loop. - -```go -for k, v := range src.Entries() { - dst.Put(k, v) -} -``` - -
- -
-ok := maps.Equal(m1, m2) - -There is no analog. - -Users have to manually check the key-value pairs to determine equality. - -
- -
-ok := maps.EqualFunc(m1, m2, fn) - -There is no analog. - -Users have to manually check the key-value pairs to determine equality. - -
- -
-maps.DeleteFunc(m, fn) - -There is no analog. - -Users have to manually delete keys. - -
- -
-it2 := maps.All(m) - -The analog is `it2 := m.Entries()`. - -
- -
-⚠️ it := maps.Keys(m) - -There is no simple analog. - -A close neighbor is `it2 := m.Entries()`. -Users can use this in a for-loop, and pick out just the keys: - -```go -for k := range m.Entries() { - // ... -} -``` - -
- -
-⚠️ it := maps.Values(m) - -There is no simple analog. - -A close neighbor is `it2 := m.Entries()`. -Users can use this in a for-loop, and pick out just the values: - -```go -for _, v := range m.Entries() { - // ... -} -``` - -
- -
-m := maps.Collect(seq) - -There is no analog. - -
- -
-maps.Insert(m, seq) - -There is no analog. - -
- -### Determining Familiarity - -We can categorize all existing table functionality by each interface's familiarity: - -- ✅ Idiomatic: is clear, intuitive, and easily understood. -- ❌ Non-idiomatic: is misleading; addressed in [Target State](#solving-congruency). - -
-m := New(opts...) - -Criteria: - -Noun/adjective form — Go constructors use NewX where X is a noun or adjective, not a verb or past participle (NewReaderSize, not NewSized) -Names what the user provides — the suffix should hint at the distinguishing parameter (NewBufferString tells you it takes a string) -Progression is readable — the three names together should imply simple → intermediate → advanced -Not misleadingly generic — NewWith or NewConfig could mean anything - -
- -
-m := NewBy(keyFunc, opts...) - -- Use `NewKeyed()`. - -
- -
-m := NewCustom(hashA, hashB, equalFunc, opts...) - -- Use `NewHashed()`. - -
- -
-seq := m.Entries() - -- Call it `All()`. - -
- -
-v := m.Find(k) - -- Call it `m.Lookup()`. The name `m.Find` implies a search algorithm. - -
- -
-v, ok := m.Get(k) - -
- -
-ok := m.Has(k) - -
- -
-err := m.Put(k, v) - -- Call it `Set()`. -- No built-in library consensus, but 3rd party packages prefer `Set()`. - -
- -
-n := m.Size() - -- Call it `Len()`. -- Size is a Java idiom. - -
- -
-str := m.String() - -
- -
-cap := m.TotalCapacity() - -- Remove. This is an implementation detail. - -
- -
-ok := m.Drop(k) - -- Call it `Delete()`. - -
- -## Target State - -### Solving Congruency - -We should make the following changes to accomodate for congruency: - -
-ok := maps.EqualFunc(m1, m2, fn) - -We should implement a new function: - -```go -func EqualFunc[K, V1, V2 any](t1 *Table[K, V1], t2 *Table[K, V2], eq func(V1, V2) bool) bool -``` - -This function is free, and not bound as a receiver function. -(It is called `cuckoo.Equal(t1, t2)`, not `t1.Equals(t2)`.) -The latter implies `t1` has authority, when in fact neither do. - -We define equality as: - -1. Neither table has a key the other doesn't. -2. Each key has the same value in each table. - Parameter `eq` determines this equality. - -Custom `EqualFunc`'s complicate this, as they modulate key identity in tables. -If two tables may differ on whether two keys are different, this function might break. -So, we must assume that: - -- Both tables have `EqualFunc`'s which 'agree' on the identity of the keys present in the tables. - Agreement is defined as: if two keys are distinct in one table, they are distinct in the other. - -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. -We will move the documentation attached to it to `DefaultEqualFunc`. - -
- -
-ok := maps.Equal(m1, m2) - -We should implement a new function, to conform with the standard library: - -```go -func Equal[K any, V comparable](t1, t2 *Table[K, V]) bool -``` - -It uses the same equality check as in `EqualFunc`. -Once again, the function is free because it is symmetric. - -
- -
-maps.Insert(m, seq) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) Insert(seq iter.Seq2[K, V]) error -``` - -A receiver fits better even though `maps.Insert` is a free function, because copying it is asymmetric. -Map `dst` receives entries from map `src`. -It's only free because Go's standard map is built into the language, and so cannot have receivers. - -In terms of naming, `t.Extend` is more accurate, and has precedent in [Python](docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [Rust](https://doc.rust-lang.org/std/iter/trait.Extend.html). -When [adding iterator function](https://github.com/golang/go/issues/61900) to the `maps` package, the Go team chose to frame it as 'sources' and 'sinks'. -With this model, `maps.Insert` made more sense than `maps.Extend`. -Ultimately, `t.Insert()` is a better choice to be consistent with `maps`. - -
- -
-maps.Copy(dst, src) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) Copy(src *Table[K, V]) error -``` - -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`. -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: - -- `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. -- `t.Merge()` implies some sort of conflict-resolution, when there is not. - It simply overwrites the values. - -
- -
-maps.DeleteFunc(m, fn) - -We should implement a new receiver for the table: - -```go -func (t *Table[K, V]) DeleteFunc(del func(K, V) bool) -``` - -It would have the same functionality as `maps.DeleteFunc`. - -A free function could work here, but `t` has clear authority over `del`. -Other than being consistent with the `maps` package, `t.DeleteFunc` follows the Go convention of appending `Func` to higher-order equivalents of functions. -This trumps names like `t.DeleteIf`, which lend more to [Java](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html#removeIf-java.util.function.Predicate-) or [C++](https://en.cppreference.com/cpp/algorithm/remove). -The word `Delete` is also convention, tying back to the built-in `delete()`. - -
- -
-m := maps.Collect(seq) - -We should implement a new constructor. - -```go -func Collect[K comparable, V any](seq iter.Seq2[K, V]) (*Table[K, V], error) -``` - -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) -``` - -
- -
-m := map[K]V{...} - -We should make a new constructor, because entries are generic. -So, creating an option with inialized entries doesn't work. - -With the previous additions, users have a few options. -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. - -
- -
-m := make(map[K]V, hint) - -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. - -
- -
-clear(m) - -We should implement a new receiver: - -```go -func (t *Table[K, V]) Clear() -``` - -It will remove all entries from the table. - -
- -
-m2 := maps.Clone(m) - -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. - -
- -
-it := maps.Keys(m) - -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. - -
- -
-it := maps.Values(m) - -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()`. - -
-- 2.52.0 From df5a25d349f229662dfb748ab00a64a7d36facfa Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:29:04 -0400 Subject: [PATCH 11/19] chore: move to docs/adr --- {adr => docs/adr}/000_template.md | 0 {adr => docs/adr}/001_design_principles.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {adr => docs/adr}/000_template.md (100%) rename {adr => docs/adr}/001_design_principles.md (100%) diff --git a/adr/000_template.md b/docs/adr/000_template.md similarity index 100% rename from adr/000_template.md rename to docs/adr/000_template.md diff --git a/adr/001_design_principles.md b/docs/adr/001_design_principles.md similarity index 100% rename from adr/001_design_principles.md rename to docs/adr/001_design_principles.md -- 2.52.0 From b96d47fa1bbc247085a8b48c9d0e07adb7dc692f Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:30:35 -0400 Subject: [PATCH 12/19] style: no number in ADR title --- docs/adr/000_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/000_template.md b/docs/adr/000_template.md index eb36d72..1a49a6e 100644 --- a/docs/adr/000_template.md +++ b/docs/adr/000_template.md @@ -1,4 +1,4 @@ -# 000: {{TITLE}} +# Title **Status**: -- 2.52.0 From 00ed7c88888c916501b38543ddd015d77e4d20ba Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 3 Jul 2026 21:41:42 -0400 Subject: [PATCH 13/19] style: capitalization in status for adr template --- docs/adr/000_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/000_template.md b/docs/adr/000_template.md index 1a49a6e..3bf9789 100644 --- a/docs/adr/000_template.md +++ b/docs/adr/000_template.md @@ -1,6 +1,6 @@ # Title -**Status**: +**Status**: ## Context -- 2.52.0 From 3bc9d63b93f514f9db5982712cecf9145482d6ab Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 19:46:36 -0400 Subject: [PATCH 14/19] docs: add link to branch with previous work --- AGENTS.md | 48 +++++++++++++++++++++++++++++++ docs/adr/001_design_principles.md | 1 + 2 files changed, 49 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8d990ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# AGENTS.md + +## Git hosting: this is Gitea, not GitHub + +`go-cuckoo` is hosted on a self-hosted Gitea instance, not GitHub. `gh` will +not work against it — use the `tea` CLI instead. + +- Remote: `git@git.maximhutz.com:tools/go-cuckoo.git` +- Repo slug for `tea`: `tools/go-cuckoo` +- PR URLs look like `https://git.maximhutz.com/tools/go-cuckoo/pulls/` + (path is `pulls`, matching `tea`'s `pulls` command — not `pull`). + +## Inspecting a PR + +Get metadata as JSON: + +```sh +tea pulls --repo tools/go-cuckoo -o json \ + -f "index,title,state,author,base,head,body,labels" +``` + +Non-interactive sessions print a one-line `NOTE: no gitea login detected, +falling back to login ''` on stderr before the JSON — expected, not an +error. + +Useful fields from the JSON: `base` / `head` (branch names), `headSha`, +`diffUrl` (`.diff`, fetchable without auth via plain `curl`/`WebFetch` +for a public repo). + +**`-f diff` and `-f patch` are listed in `tea pulls --help` but are silently +dropped from JSON output — verified empirically, don't rely on them.** To get +the actual diff: + +```sh +git fetch origin +git diff origin/...HEAD # only valid if local HEAD == PR's headSha +``` + +Check local `HEAD` against the PR's `headSha` first (`git rev-parse HEAD`) — +if they differ, the checkout isn't the PR branch and this diff is wrong. +Otherwise fall back to fetching `diffUrl` directly. + +## Other `tea` subcommands that exist + +`tea pulls list|checkout|create|review|approve|reject|merge|review-comments`, +plus top-level `tea issues`, `tea releases`, `tea labels`. Run +`tea --help` before assuming GitHub-shaped flags carry over — Gitea's +CLI has its own field names and defaults (e.g. `--state` defaults to `open`). diff --git a/docs/adr/001_design_principles.md b/docs/adr/001_design_principles.md index 43dd41f..5c7bc46 100644 --- a/docs/adr/001_design_principles.md +++ b/docs/adr/001_design_principles.md @@ -33,6 +33,7 @@ Do not equate them. - [ ] Update the `README.md` and `doc.go` to reflect these principles. - [ ] Update the contributing guide and pull request template to require these principles are met. 2. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. + (I already started work on branch `docs/interface-congruency-analysis`.) I should prioritize limiting any incongruencies. - [ ] Produce the first draft to uncover any current incongruencies. - [ ] Link the document to the `README.md`. -- 2.52.0 From 9b5545bf52acb2e0dc6fdfe3565f17e3e3dff50e Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 19:48:23 -0400 Subject: [PATCH 15/19] style: reordered second consequence --- docs/adr/001_design_principles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/001_design_principles.md b/docs/adr/001_design_principles.md index 5c7bc46..cb782dd 100644 --- a/docs/adr/001_design_principles.md +++ b/docs/adr/001_design_principles.md @@ -33,8 +33,8 @@ Do not equate them. - [ ] Update the `README.md` and `doc.go` to reflect these principles. - [ ] Update the contributing guide and pull request template to require these principles are met. 2. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. - (I already started work on branch `docs/interface-congruency-analysis`.) I should prioritize limiting any incongruencies. + (I already started work on branch `docs/interface-congruency-analysis`.) - [ ] Produce the first draft to uncover any current incongruencies. - [ ] Link the document to the `README.md`. 3. Analyze the familiarity of `go-cuckoo`'s current interface. -- 2.52.0 From 0b3223b916c2034426ec9176d2a0738bbcf9d657 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 20:01:34 -0400 Subject: [PATCH 16/19] docs: articulate core functionality --- docs/adr/001_design_principles.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/adr/001_design_principles.md b/docs/adr/001_design_principles.md index cb782dd..530aac2 100644 --- a/docs/adr/001_design_principles.md +++ b/docs/adr/001_design_principles.md @@ -16,10 +16,11 @@ While the implementation does work, it lacks direction. To resolve this, I'm enforcing two new principles onto the contract of `go-cuckoo`: - **Congruency**: - A `go-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 `map`. + By 'core', I mean `map`'s built-in syntax and functions (e.g. `range`, `m[k]`), and the `maps` package. - **Familiarity**: - A `go-cuckoo` table should behave similarly to Go's standard map, so users will intuitively know how to use it. + A `go-cuckoo` table should behave similarly to `map`, so users will intuitively know how to use it. In effect, its users will carry less cognitive load. These principles should _guide_ the public interface of `go-cuckoo`. -- 2.52.0 From 872020429857130d7146135a35d68f7572587920 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 20:03:58 -0400 Subject: [PATCH 17/19] fix: exemplify differences between map and go-cuckoo table --- docs/adr/001_design_principles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/001_design_principles.md b/docs/adr/001_design_principles.md index 530aac2..a155ef9 100644 --- a/docs/adr/001_design_principles.md +++ b/docs/adr/001_design_principles.md @@ -25,7 +25,7 @@ To resolve this, I'm enforcing two new principles onto the contract of `go-cucko These principles should _guide_ the public interface of `go-cuckoo`. Neither should be treated absolutely, though. -The behavior of `go-cuckoo` is distinct from `map`. +The behavior of `go-cuckoo` is distinct from `map` (e.g. `Put` can fail; see `ErrBadHash`). Do not equate them. ## Consequences -- 2.52.0 From 7b0f0168338909c3d7ccf6038006b5e51d4e03b0 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 21:19:25 -0400 Subject: [PATCH 18/19] docs: rename principles ot match more fundamental principles --- docs/adr/001_design_principles.md | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/adr/001_design_principles.md b/docs/adr/001_design_principles.md index a155ef9..8ca4744 100644 --- a/docs/adr/001_design_principles.md +++ b/docs/adr/001_design_principles.md @@ -1,6 +1,6 @@ -# Adopt Congruent and Familiar Design For `go-cuckoo` +# Adopt Parity and Consistency as Principles For `go-cuckoo` -**Status**: Proposed +**Status**: Accepted ## Context @@ -15,32 +15,32 @@ While the implementation does work, it lacks direction. To resolve this, I'm enforcing two new principles onto the contract of `go-cuckoo`: -- **Congruency**: +- **Parity (with `map`)**: A `go-cuckoo` table should have the same core functionality as `map`. By 'core', I mean `map`'s built-in syntax and functions (e.g. `range`, `m[k]`), and the `maps` package. + Higher parity means users can trust that `go-cuckoo` can do what `map` can do. -- **Familiarity**: +- **Consistency (with `map`)**: A `go-cuckoo` table should behave similarly to `map`, so users will intuitively know how to use it. - In effect, its users will carry less cognitive load. + Higher consistency lowers the cognitive load users must carry. -These principles should _guide_ the public interface of `go-cuckoo`. -Neither should be treated absolutely, though. -The behavior of `go-cuckoo` is distinct from `map` (e.g. `Put` can fail; see `ErrBadHash`). -Do not equate them. +While these principles should guide the interface of `go-cuckoo`, they should not be absolute. +The behavior of `go-cuckoo` is distinct from `map` (e.g. `Put` can fail; see `ErrBadHash`), so do not equate them. ## Consequences -1. The repository should support both design principles. - - [ ] Update the `README.md` and `doc.go` to reflect these principles. - - [ ] Update the contributing guide and pull request template to require these principles are met. -2. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. - I should prioritize limiting any incongruencies. +1. The repository should contain a living document, describing the interface differences between `go-cuckoo` and `map`. + I should prioritize limiting any disparity. (I already started work on branch `docs/interface-congruency-analysis`.) - - [ ] Produce the first draft to uncover any current incongruencies. + - [ ] Produce the first draft to uncover any current disparity. - [ ] Link the document to the `README.md`. -3. Analyze the familiarity of `go-cuckoo`'s current interface. - Unlike the analysis of congruency, this should be a one time document. - Familiarity is implicit to users, and does not need to be referenced. +2. Analyze the consistency of `go-cuckoo`'s current interface. + Unlike the analysis of parity, this should be a one time document. + Consistency is implicit to users, and does not need to be referenced. But, any rationale should be documented in commit messages, or future ADRs. - [ ] Produce the analysis document. - [ ] Resolve any issues found. +3. The repository should support both design principles. + As I resolve gaps in parity and consistency, I should feed any reusable heuristics back into the contributing guide. + - [ ] State these principles in the `README.md` and `doc.go`. + - [ ] Ground the contributing guide and pull request template in these new principles. -- 2.52.0 From 192ca9d85374c697f85304b26b583aabc3f77900 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Jul 2026 21:22:40 -0400 Subject: [PATCH 19/19] revert: no agents.md in this pr --- AGENTS.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 8d990ca..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,48 +0,0 @@ -# AGENTS.md - -## Git hosting: this is Gitea, not GitHub - -`go-cuckoo` is hosted on a self-hosted Gitea instance, not GitHub. `gh` will -not work against it — use the `tea` CLI instead. - -- Remote: `git@git.maximhutz.com:tools/go-cuckoo.git` -- Repo slug for `tea`: `tools/go-cuckoo` -- PR URLs look like `https://git.maximhutz.com/tools/go-cuckoo/pulls/` - (path is `pulls`, matching `tea`'s `pulls` command — not `pull`). - -## Inspecting a PR - -Get metadata as JSON: - -```sh -tea pulls --repo tools/go-cuckoo -o json \ - -f "index,title,state,author,base,head,body,labels" -``` - -Non-interactive sessions print a one-line `NOTE: no gitea login detected, -falling back to login ''` on stderr before the JSON — expected, not an -error. - -Useful fields from the JSON: `base` / `head` (branch names), `headSha`, -`diffUrl` (`.diff`, fetchable without auth via plain `curl`/`WebFetch` -for a public repo). - -**`-f diff` and `-f patch` are listed in `tea pulls --help` but are silently -dropped from JSON output — verified empirically, don't rely on them.** To get -the actual diff: - -```sh -git fetch origin -git diff origin/...HEAD # only valid if local HEAD == PR's headSha -``` - -Check local `HEAD` against the PR's `headSha` first (`git rev-parse HEAD`) — -if they differ, the checkout isn't the PR branch and this diff is wrong. -Otherwise fall back to fetching `diffUrl` directly. - -## Other `tea` subcommands that exist - -`tea pulls list|checkout|create|review|approve|reject|merge|review-comments`, -plus top-level `tea issues`, `tea releases`, `tea labels`. Run -`tea --help` before assuming GitHub-shaped flags carry over — Gitea's -CLI has its own field names and defaults (e.g. `--state` defaults to `open`). -- 2.52.0