comparable and cmp.Ordered — Tasks¶
Exercise structure¶
- 🟢 Easy — for beginners
- 🟡 Medium — middle level
- 🔴 Hard — senior level
- 🟣 Expert — professional level
A solution for each exercise is provided at the end. The tasks emphasize picking the right constraint — many ask you to compare a comparable solution with a cmp.Ordered one.
Easy 🟢¶
Task 1 — Build Set[T comparable]¶
Write a generic set with Add(v T), Has(v T) bool, Remove(v T), and Len() int. Why must T be comparable?
Task 2 — Contains for comparable¶
Write Contains[T comparable](s []T, v T) bool. Compare with slices.Contains.
Task 3 — Min and Max over Ordered¶
Write Min[T cmp.Ordered](a, b T) T and Max[T cmp.Ordered](a, b T) T. Show that they fail with comparable.
Task 4 — Distinct preserving order¶
Write Distinct[T comparable](s []T) []T that removes duplicates while preserving the first occurrence.
Task 5 — Coalesce¶
Write Coalesce[T comparable](vals ...T) T returning the first non-zero. Compare with cmp.Or.
Medium 🟡¶
Task 6 — SortedSlice[T cmp.Ordered]¶
Build a wrapper around []T that maintains sorted order on Insert. Provide Has (binary search), Insert, and Range(lo, hi T) []T.
Task 7 — Top-N¶
Write TopN[T cmp.Ordered](s []T, n int) []T returning the N largest. Discuss using a min-heap of size N.
Task 8 — Generic frequency count¶
Write Counts[T comparable](s []T) map[T]int. What constraint does T need?
Task 9 — NaN-safe sort¶
Sort []float64 containing NaN values using slices.Sort. Verify NaN is at the front.
Task 10 — Sort users by Age then Name¶
Given type User struct { Age int; Name string }, sort using slices.SortFunc and cmp.Or for tie-breaking.
Task 11 — Set from any iterable¶
Write SetOf[T comparable](items ...T) *Set[T]. Use it to build a set from a slice in one line.
Task 12 — Equal for slices of comparable¶
Write SlicesEqual[T comparable](a, b []T) bool. Compare with slices.Equal.
Task 13 — Range over cmp.Ordered¶
Write Between[T cmp.Ordered](v, lo, hi T) bool checking lo <= v <= hi.
Task 14 — Sort []time.Time¶
Sort a slice of time.Time values. Why does slices.Sort not work directly? Use slices.SortFunc.
Hard 🔴¶
Task 15 — MinHeap[T cmp.Ordered]¶
Implement a min-heap with Push(v T) and Pop() T. Use cmp.Compare internally so NaN works for floats.
Task 16 — Generic BST[T cmp.Ordered]¶
Build a binary search tree with Insert, Has, and InOrder() []T. Discuss what happens for T = float64 with NaN.
Task 17 — MultiSet[T comparable]¶
A bag that counts occurrences. Add, Remove (decrement), Count(v T) int, and Total() int. Why does this not need cmp.Ordered?
Task 18 — OrderedCache[K cmp.Ordered, V any]¶
A cache that supports range scans Scan(lo, hi K) []V. What stays in comparable, what bumps up to Ordered?
Task 19 — Strictly comparable filter¶
Given s []any, return only elements whose dynamic type is strictly comparable. Use reflect.TypeOf(v).Comparable().
Expert 🟣¶
Task 20 — cmp.Compare-based total order on complex128¶
Implement CompareComplex(a, b complex128) int using magnitude as the order. Sort []complex128 with it.
Task 21 — Custom NaN handling¶
Write SortNaNLast[T cmp.Ordered](s []T) that places NaN at the end instead of the front. Use a comparator with explicit NaN detection.
Task 22 — Hash-based set for non-comparable¶
Build HashSet[T any] that takes a hash func(T) uint64 and equal func(T, T) bool. Use it to deduplicate []byte values, which are not comparable.
Solutions¶
Solution 1¶
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] { return &Set[T]{m: map[T]struct{}{}} }
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
func (s *Set[T]) Remove(v T) { delete(s.m, v) }
func (s *Set[T]) Len() int { return len(s.m) }
T comparable is required because the underlying map[T]struct{} requires comparable keys. Solution 2¶
func Contains[T comparable](s []T, v T) bool {
for _, x := range s {
if x == v { return true }
}
return false
}
slices.Contains is identical, with constraint [S ~[]E, E comparable]. Solution 3¶
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
func Max[T cmp.Ordered](a, b T) T {
if a > b { return a }
return b
}
comparable, the < operator is rejected at compile time. Solution 4¶
func Distinct[T comparable](s []T) []T {
seen := map[T]struct{}{}
out := make([]T, 0, len(s))
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
return out
}
Solution 5¶
func Coalesce[T comparable](vals ...T) T {
var zero T
for _, v := range vals {
if v != zero { return v }
}
return zero
}
cmp.Or(vals...). Solution 6¶
import (
"cmp"
"slices"
)
type SortedSlice[T cmp.Ordered] struct {
data []T
}
func (s *SortedSlice[T]) Insert(v T) {
i, _ := slices.BinarySearch(s.data, v)
s.data = slices.Insert(s.data, i, v)
}
func (s *SortedSlice[T]) Has(v T) bool {
_, ok := slices.BinarySearch(s.data, v)
return ok
}
func (s *SortedSlice[T]) Range(lo, hi T) []T {
i, _ := slices.BinarySearch(s.data, lo)
j, _ := slices.BinarySearch(s.data, hi)
if j < len(s.data) && s.data[j] == hi { j++ }
return slices.Clone(s.data[i:j])
}
Solution 7¶
import (
"cmp"
"slices"
)
func TopN[T cmp.Ordered](s []T, n int) []T {
sorted := slices.Clone(s)
slices.SortFunc(sorted, func(a, b T) int { return cmp.Compare(b, a) })
if n > len(sorted) { n = len(sorted) }
return sorted[:n]
}
n << len(s). Solution 8¶
func Counts[T comparable](s []T) map[T]int {
m := map[T]int{}
for _, v := range s {
m[v]++
}
return m
}
comparable is required because T is a map key. Solution 9¶
import (
"math"
"slices"
)
xs := []float64{3, math.NaN(), 1, math.NaN(), 2}
slices.Sort(xs)
// Output: [NaN NaN 1 2 3]
slices.Sort uses cmp.Compare internally, which puts NaN at the front. Solution 10¶
import (
"cmp"
"slices"
)
slices.SortFunc(users, func(a, b User) int {
return cmp.Or(
cmp.Compare(a.Age, b.Age),
cmp.Compare(a.Name, b.Name),
)
})
Solution 11¶
func SetOf[T comparable](items ...T) *Set[T] {
s := NewSet[T]()
for _, v := range items { s.Add(v) }
return s
}
s := SetOf(1, 2, 3, 2, 1) // {1, 2, 3}
Solution 12¶
func SlicesEqual[T comparable](a, b []T) bool {
if len(a) != len(b) { return false }
for i := range a {
if a[i] != b[i] { return false }
}
return true
}
slices.Equal. Solution 13¶
Solution 14¶
import (
"slices"
"time"
)
slices.SortFunc(times, func(a, b time.Time) int {
return a.Compare(b) // time.Time has Compare method since Go 1.20
})
slices.Sort does not work because time.Time is a struct, not in cmp.Ordered. Solution 15¶
import "cmp"
type MinHeap[T cmp.Ordered] struct{ data []T }
func (h *MinHeap[T]) Push(v T) {
h.data = append(h.data, v)
i := len(h.data) - 1
for i > 0 {
parent := (i - 1) / 2
if cmp.Compare(h.data[i], h.data[parent]) >= 0 { break }
h.data[i], h.data[parent] = h.data[parent], h.data[i]
i = parent
}
}
func (h *MinHeap[T]) Pop() (T, bool) {
var zero T
if len(h.data) == 0 { return zero, false }
top := h.data[0]
last := len(h.data) - 1
h.data[0] = h.data[last]
h.data = h.data[:last]
h.heapifyDown(0)
return top, true
}
func (h *MinHeap[T]) heapifyDown(i int) {
n := len(h.data)
for {
l, r := 2*i+1, 2*i+2
smallest := i
if l < n && cmp.Compare(h.data[l], h.data[smallest]) < 0 { smallest = l }
if r < n && cmp.Compare(h.data[r], h.data[smallest]) < 0 { smallest = r }
if smallest == i { return }
h.data[i], h.data[smallest] = h.data[smallest], h.data[i]
i = smallest
}
}
cmp.Compare ensures NaN does not break heap invariants for T = float64. Solution 16¶
import "cmp"
type bnode[T cmp.Ordered] struct {
v T
left, right *bnode[T]
}
type BST[T cmp.Ordered] struct{ root *bnode[T] }
func (t *BST[T]) Insert(v T) { t.root = insert(t.root, v) }
func insert[T cmp.Ordered](n *bnode[T], v T) *bnode[T] {
if n == nil { return &bnode[T]{v: v} }
switch cmp.Compare(v, n.v) {
case -1: n.left = insert(n.left, v)
case 1: n.right = insert(n.right, v)
}
return n
}
float64, NaN is treated by cmp.Compare as less than every non-NaN, so the tree stays well-formed. Solution 17¶
type MultiSet[T comparable] struct {
m map[T]int
}
func NewMultiSet[T comparable]() *MultiSet[T] { return &MultiSet[T]{m: map[T]int{}} }
func (s *MultiSet[T]) Add(v T) { s.m[v]++ }
func (s *MultiSet[T]) Remove(v T) {
if s.m[v] > 1 { s.m[v]--; return }
delete(s.m, v)
}
func (s *MultiSet[T]) Count(v T) int { return s.m[v] }
func (s *MultiSet[T]) Total() int {
n := 0
for _, c := range s.m { n += c }
return n
}
Solution 18¶
type OrderedCache[K cmp.Ordered, V any] struct {
keys []K
m map[K]V
}
func (c *OrderedCache[K, V]) Set(k K, v V) {
if _, ok := c.m[k]; !ok {
i, _ := slices.BinarySearch(c.keys, k)
c.keys = slices.Insert(c.keys, i, k)
}
if c.m == nil { c.m = map[K]V{} }
c.m[k] = v
}
func (c *OrderedCache[K, V]) Scan(lo, hi K) []V {
i, _ := slices.BinarySearch(c.keys, lo)
out := []V{}
for ; i < len(c.keys) && c.keys[i] <= hi; i++ {
out = append(out, c.m[c.keys[i]])
}
return out
}
K is cmp.Ordered because of the range scan; equality alone would not allow <=. Solution 19¶
import "reflect"
func StrictlyComparable(s []any) []any {
out := []any{}
for _, v := range s {
if v == nil { continue }
if reflect.TypeOf(v).Comparable() {
out = append(out, v)
}
}
return out
}
reflect.TypeOf(...).Comparable() is the runtime check for "is this strictly comparable". Solution 20¶
import (
"cmp"
"math/cmplx"
"slices"
)
func CompareComplex(a, b complex128) int {
return cmp.Compare(cmplx.Abs(a), cmplx.Abs(b))
}
slices.SortFunc(cs, CompareComplex)
Solution 21¶
import (
"cmp"
"math"
"slices"
)
func SortNaNLast(s []float64) {
slices.SortFunc(s, func(a, b float64) int {
aNaN := math.IsNaN(a)
bNaN := math.IsNaN(b)
switch {
case aNaN && bNaN: return 0
case aNaN: return 1 // NaN goes after
case bNaN: return -1
default: return cmp.Compare(a, b)
}
})
}
Solution 22¶
type HashSet[T any] struct {
buckets map[uint64][]T
hash func(T) uint64
equal func(T, T) bool
}
func NewHashSet[T any](hash func(T) uint64, equal func(T, T) bool) *HashSet[T] {
return &HashSet[T]{
buckets: map[uint64][]T{},
hash: hash,
equal: equal,
}
}
func (s *HashSet[T]) Add(v T) {
h := s.hash(v)
for _, x := range s.buckets[h] {
if s.equal(x, v) { return }
}
s.buckets[h] = append(s.buckets[h], v)
}
// Usage for []byte:
import "hash/fnv"
import "bytes"
byteHash := func(b []byte) uint64 {
h := fnv.New64()
h.Write(b)
return h.Sum64()
}
byteEq := func(a, b []byte) bool { return bytes.Equal(a, b) }
set := NewHashSet[[]byte](byteHash, byteEq)
Final notes¶
The recurring lesson: constraint follows operation.
Add/Hason a map →comparableSort/Min/Max→cmp.Orderedtime.Time,complex, structs →slices.SortFuncwithcmp.Compare[]byte,[]int, slice keys →HashSetwith explicit hash and equal
Treat comparable and cmp.Ordered as the two heaviest gears in your generic toolbox. Reach for them by default; reach past them when the type does not fit.