In which the Squirrel discovers generics and Oskar says no
Previously on The Hundred Forms…
The Labyrinth of a Hundred Forms had tamed the pointer bug. Two objects go in, two different values come out. The forms were safe.
But safety breeds confidence. And confidence breeds… looking around.
The Squirrel was comparing files, as Squirrels do when they should be shipping.
“Look at this,” she muttered, tail twitching. “The credential FormData and the store FormData. They’re IDENTICAL.”
// domains/credential/form.templ
type FormData struct {
IsNew bool
Errors components.FieldErrors
DraftURL string
History gitstore.HistorySummary
RestoreVersion string
Original *Credential
Current *Credential
}
// domains/store/form.templ
type FormData struct {
IsNew bool
Errors components.FieldErrors
DraftURL string
History gitstore.HistorySummary
RestoreVersion string
Original *Store
Current *Store
}
“Five fields. Copied. And we’re building a hundred forms.”
Her eyes began to gleam with that dangerous light. The light that meant ABSTRACTION.
The Squirrel’s Solution
“GENERICS!” The Squirrel leaped onto the keyboard. “Go 1.18 gave us generics! We can finally do this RIGHT!”
// The Squirrel's vision
type FormData[T any] struct {
IsNew bool
Errors components.FieldErrors
DraftURL string
History gitstore.HistorySummary
RestoreVersion string
Original *T
Current *T
}
// Then in each domain:
type CredentialForm = FormData[Credential]
type StoreForm = FormData[Store]
“One definition! Type-safe! ELEGANT!”
She was already sketching the generic methods:
func (f FormData[T]) HasChanges() bool {
return !reflect.DeepEqual(f.Original, f.Current) // π reflect
}
func (f FormData[T]) Status() layouts.ActionType {
// But wait... each domain has different comparison logic...
// Credential compares specific fields...
// Store has nested connection structs...
}
The Squirrel paused. “We’d need… interfaces. A Comparable interface. Each type implements HasChanges() on itself, then FormData calls it…”
type Comparable interface {
HasChanges(other any) bool
}
func (f FormData[T]) HasChanges() bool {
return f.Current.(Comparable).HasChanges(f.Original)
}
“Type assertions,” she muttered. “Runtime checks. But it’s worth it for the ABSTRACTION.”
Oskar Speaks
A massive orange shape materialized on the warm spot by the monitor. Oskar, the Maine Coon, regarded the Squirrel with amber eyes that held ancient wisdom. Or possibly just hunger. With cats, it’s hard to tell.
“The Lizard sends a message,” Oskar said, stretching luxuriously.
“The Lizard? He’s been quiet all day.”
“He’s been watching.” Oskar yawned, revealing impressive fangs. “He says: what if you embedded instead of abstracted?”
“Embedded?”
Oskar placed one massive paw on the keyboard:
// ui/forms/state.go
type State struct {
IsNew bool
Errors components.FieldErrors
DraftURL string
History gitstore.HistorySummary
RestoreVersion string
}
// domains/credential/form.templ
type FormData struct {
forms.State // π Embedded. That's it.
Original *Credential
Current *Credential
}
The Squirrel blinked. “But… that’s not type-safe for Original and Current.”
“Do they need to be?” Oskar licked a paw. “Each domain knows its own types. You’re not passing FormData between domains. You just want to stop copying the same five fields.”
“But the methods! HasChanges, Status, Footer state machine…”
// The behavior lives on State
func (s State) IsRestoring() bool {
return s.RestoreVersion != ""
}
func (s State) Footer(hasChanges bool) FooterState {
if s.IsRestoring() { return FooterRestoring }
if s.IsNew {
if hasChanges { return FooterNewDirty }
return FooterNewClean
}
if hasChanges { return FooterEditDirty }
return FooterEditClean
}
// Each domain wraps with its own HasChanges
func (d FormData) HasChanges() bool {
return d.Original.ID != d.Current.ID ||
d.Original.Name != d.Current.Name // ... domain-specific
}
func (d FormData) Status() layouts.ActionType {
return d.State.Status(d.HasChanges()) // π Delegate
}
“The generic parts are generic,” Oskar concluded. “The specific parts stay specific. No reflection. No interfaces. No type assertions.”
The Arithmetic
The Squirrel did the comparison:
| Approach | Lines | Runtime Cost | Complexity |
|---|---|---|---|
| Copy-paste | 6 fields Γ 100 forms = 600 | None | Low but tedious |
| Generics | 1 definition + interfaces | Reflection | High |
| Embedding | 1 State + 100 one-liners | None | Low |
“Embedding is… simpler,” she admitted.
“The Lizard says: generics are for when you need to write algorithms that work on any type. You don’t have an algorithm. You have shared fields.”
“But generics are more ELEGANTβ”
“The Lizard says: elegant is a word Squirrels use to justify complexity.”
The Bonus Round
While extracting State, they found another refugee: slugify(). Two identical implementations, one in each domain.
// Extracted to ui/forms/slug.go
func Slugify(s string) string {
s = strings.ToLower(s)
s = strings.ReplaceAll(s, " ", "-")
// ... keep only a-z, 0-9, hyphen
}
And then the Squirrel noticed the field update handlers. A hundred lines of switch statements, parsing form values into struct fields.
“What if we used reflection JUST for the boring part?” she asked carefully.
Oskar’s ears perked up. The Lizard, it seemed, approved of surgical reflection.
// ui/forms/field.go - reflection for the tedious part only
func SetField(target any, fieldName, value string) bool {
// Uses form:"name" struct tags
// Handles string, bool, int, time.Time conversion
}
// Handler becomes:
value := r.FormValue(field)
forms.SetField(&cred, field, value)
// Side effects stay explicit:
switch field {
case "name":
if id == "new" { cred.ID = forms.Slugify(value) }
case "owner":
if value != "" { cred.ADGroup = "" } // Mutually exclusive
}
“Reflection for assignment,” Oskar nodded. “Switch for side effects. Each tool for its purpose.”
The Lesson
The Squirrel pinned a new note next to yesterday’s:
Generics: when you need algorithms that work on any type.
Embedding: when you need to share fields and behavior.
The difference is whether you’re abstracting OVER types or COMPOSING with types.
Oskar stretched and padded toward the kitchen. “The Lizard also says: sometimes the old ways are old because they work. Go had embedding before generics. There was a reason.”
“What’s the reason?”
“Simplicity. The same reason HTMX beats React. The same reason server-is-truth beats client-state. The same reason you’re building V4 instead of maintaining V3.”
The Squirrel looked at her generic scaffolding, half-built and already creaking with interfaces.
She deleted it.
Next time: The Squirrel discovers that validation rules are just data, and the Lizard approves
See also:
Today’s commits:
f078777- Extract forms.State for embeddingd9a0f72- Add forms.SetField for tag-based field assignment4ba0af4- Apply SetField to store domain3f5e139- Fix validation error keys (name, not label)
The Hundred Forms Saga:
- The Labyrinth of a Hundred Forms - LoadDraftAndOriginal and the pointer bug
- The Labyrinth of a Hundred Forms - Navigation that multiplied
- The Hundred Forms - A Prophecy Revealed - 45 seconds per form
The Characters:
- The Lizard Brain vs The Caffeinated Squirrel - The eternal YAGNI struggle
The References:
- Go Embedding - Composition over inheritance
- When to use generics - The Go team’s guidance
The Deeper Wisdom (in which giants speak):
- Simplicity is Complicated - Rob Pike explains why Go chose embedding over inheritance, and simplicity over features (GopherCon 2015)
- Go Proverbs - “The bigger the interface, the weaker the abstraction” and other truths
- Don’t Use Go’s Default HTTP Client - Nothing to do with forms, but the same philosophy: defaults that make the wrong thing easy are bugs
