Recursive Type Constraints — Find the Bug¶
How to use¶
Each problem shows a code snippet. Read it carefully and answer: 1. What is the bug? 2. How would you fix it? 3. Would a different abstraction have prevented it?
Solutions are at the end. Many of these bugs come from real codebases experimenting with F-bounded polymorphism.
Bug 1 — Cloner returning the interface, not T¶
type Cloner[T any] interface {
Clone() Cloner[T]
}
func DupAll[T Cloner[T]](xs []T) []T {
out := make([]T, len(xs))
for i, v := range xs { out[i] = v.Clone() }
return out
}
Hint: What is the return type of Clone()?
Bug 2 — Forgetting the recursion¶
type Cloner interface {
Clone() Cloner
}
func DupAll[T Cloner](xs []T) []T {
out := make([]T, len(xs))
for i, v := range xs { out[i] = v.Clone() }
return out
}
Hint: Which T is Clone returning?
Bug 3 — Wrong receiver type¶
type Cloner[T any] interface { Clone() T }
type User struct{ Name string }
func (u *User) Clone() User { return User{Name: u.Name} }
func main() {
xs := []User{{"Ada"}}
DupAll(xs) // ❌
}
Hint: Which method set has Clone?
Bug 4 — Cloning a pointer slice with value method¶
type Cloner[T any] interface { Clone() T }
type User struct{ Name string }
func (u User) Clone() User { return User{u.Name} }
func main() {
xs := []*User{{Name: "Ada"}}
DupAll(xs) // ❌
}
Hint: What is the type of the slice elements?
Bug 5 — Method type parameter¶
type Box[T any] struct{ v T }
func (b Box[T]) Map[U any](f func(T) U) Box[U] {
return Box[U]{v: f(b.v)}
}
Hint: What does Go forbid for methods?
Bug 6 — Constraint with any instead of T¶
type Cloner[T any] interface { Clone() T }
func DupAll[T Cloner[any]](xs []T) []T {
out := make([]T, len(xs))
for i, v := range xs { out[i] = v.Clone() }
return out
}
Hint: What does v.Clone() return?
Bug 7 — Nested recursion¶
type Cloner[T any] interface { Clone() T }
func DeepDup[T Cloner[Cloner[T]]](xs []T) []T {
out := make([]T, len(xs))
for i, v := range xs { out[i] = v.Clone() }
return out
}
Hint: Read the constraint slowly.
Bug 8 — Inference failure on two-parameter recursive bound¶
type Pairable[A, B any] interface {
Pair(other A) B
}
func PairAll[A Pairable[A, B], B any](xs []A) []B {
out := []B{}
for i := 0; i+1 < len(xs); i += 2 {
out = append(out, xs[i].Pair(xs[i+1]))
}
return out
}
func main() {
var xs []int
PairAll(xs) // ❌
}
Hint: Does int have a Pair method? What can the compiler infer?
Bug 9 — Comparing a value of T inside the body¶
type Comparable[T any] interface {
CompareTo(other T) int
}
func Equal[T Comparable[T]](a, b T) bool {
return a == b // ❌
}
Hint: What does Comparable[T] allow?
Bug 10 — Mixing comparable and recursion incorrectly¶
type Cloner[T comparable] interface {
Clone() T
}
func DupAll[T Cloner[T]](xs []T) []T {
out := make([]T, len(xs))
for i, v := range xs { out[i] = v.Clone() }
return out
}
type SliceOfInt []int
// SliceOfInt has Clone but is not comparable.
Hint: What does the inner constraint require?
Bug 11 — Builder returning the interface¶
type Stepper[B any] interface {
Step() Stepper[B]
}
type IntBuilder struct{ V int }
func (b IntBuilder) Step() Stepper[IntBuilder] {
return IntBuilder{V: b.V + 1}
}
Hint: What is the return type of Step()?
Bug 12 — Forgetting to instantiate a recursive type¶
Hint: Generic types need type arguments.
Bug 13 — Wrong T in receiver method list¶
Hint: Method receivers and generic types.
Bug 14 — Cloning recursion that loops forever at runtime¶
type Cloner[T any] interface { Clone() T }
type Bad struct{ self *Bad }
func (b *Bad) Clone() *Bad {
return b.self.Clone() // infinite recursion
}
Hint: Compile vs runtime.
Bug 15 — Pointer T with value satisfaction¶
type Cloner[T any] interface { Clone() T }
type S struct{}
func (s S) Clone() S { return s }
func main() {
var x *S
DupAll([]*S{x}) // ❌
}
Hint: Method sets of S vs *S.
Solutions¶
Bug 1 — fix¶
Clone() returning the interface defeats the recursion. Fix:
Bug 2 — fix¶
The interface is non-generic; Clone returns Cloner and the assignment to out[i] (typed T) fails. Fix: make the interface generic and use [T Cloner[T]].
Bug 3 — fix¶
Either change the receiver to value:
Or call with[]*User: Bug 4 — fix¶
For T = *User, the constraint Cloner[*User] requires a Clone() *User. Add:
Bug 5 — fix¶
Methods cannot declare their own type parameters. Move to a free function:
Bug 6 — fix¶
Cloner[any] returns any, not T. Use Cloner[T]:
Bug 7 — fix¶
Two layers of recursion is rejected by Go's constraint system in most shapes. Flatten to one level:
For genuine "container of cloneables", split into two type parameters explicitly.Bug 8 — fix¶
The compiler cannot infer B because int has no Pair method. Either give int a wrapper type with Pair, or instantiate explicitly:
Bug 9 — fix¶
Comparable[T] does not include comparable. Use the method:
Bug 10 — fix¶
Either drop comparable from Cloner's declaration, or accept that SliceOfInt cannot satisfy it (slices are not comparable). Best: keep Cloner[T any] and intersect with comparable in callers that need equality.
Bug 11 — fix¶
The interface returns itself, not B. Fix:
type Stepper[B any] interface { Step() B }
func (b IntBuilder) Step() IntBuilder { return IntBuilder{V: b.V + 1} }
Bug 12 — fix¶
Provide the type argument:
WhereUser satisfies Cloner[User]. Bug 13 — fix¶
The receiver must repeat the type parameter list:
Bug 14 — fix¶
Recursive constraints are a type-system mechanism. Runtime infinite loops are unrelated and your responsibility:
Bug 15 — fix¶
*S does not have Clone() *S because S's value-receiver method does not appear in *S's method set in the right shape. Add a pointer-receiver method:
Lessons¶
Patterns from these bugs:
- Return type is everything. A recursive constraint is wasted if the method returns the interface or a different parameterisation. Always return
T. - Receiver types matter. Value vs pointer receivers determine which side of T satisfies the constraint.
- Methods cannot have their own type parameters. Always use free functions.
anyin the constraint kills the recursion. Use the parameter, notany.- Nested recursion is rejected. Keep recursion shallow.
- Inference has limits. When parameters appear only in constraints, instantiate explicitly.
comparableand recursive constraints can mix, but be careful what types satisfy both.- Generic types need their type arguments. No bare
Tree.
A useful mantra: the recursion exists to bring T back to the surface. If the body of the function does not actually use the returned T as T, the constraint is misapplied. Keep the recursion meaningful.