esc
The Embedding of State
The Solid Convergence

The Embedding of State

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...

December 16, 2025

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 embedding
  • d9a0f72 - Add forms.SetField for tag-based field assignment
  • 4ba0af4 - Apply SetField to store domain
  • 3f5e139 - Fix validation error keys (name, not label)

The Hundred Forms Saga:

The Characters:

The References:

The Deeper Wisdom (in which giants speak):