Skip to content

Sealed Interfaces — 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.


Easy 🟢

Task 1 — A first sealed interface

Define a sealed Shape interface using an unexported marker method (isShape()). Implement two variants: Circle{Radius float64} and Square{Side float64}.

Task 2 — Sealed Expr (Number, BinOp, Var)

Build a sealed Expr for a calculator AST with Number{Value float64}, BinOp{Op rune; Left, Right Expr}, Var{Name string}. The interface must be sealed via an unexported method so no outside package can add new expression kinds.

Task 3 — Constructors for sealed Expr

Add helpers Num(v float64) Expr, Bin(op rune, l, r Expr) Expr, V(name string) Expr so callers never reach for the struct literals.

Task 4 — Pretty printer with type switch

Write Print(e Expr) string that uses a type switch to render Number{2} → "2", Var{x} → "x", BinOp{+, 1, 2} → "(1 + 2)".

Task 5 — Sealed Direction enum-like type

Replace an int enum with a sealed Direction with North, South, East, West zero-sized struct variants. The compiler must reject any direction defined outside the package.


Medium 🟡

Task 6 — Eval method via type switch

Add Eval(e Expr, env map[string]float64) (float64, error) for the Expr from Task 2. Use a single type switch; return an error for an unknown variable.

Task 7 — Payment events: Captured / Refunded / Failed

Define a sealed PaymentEvent with Captured{ID string; Amount int}, Refunded{ID string; Amount int}, Failed{ID string; Reason string}. Write Apply(events []PaymentEvent) int that folds events into a balance (Captured adds, Refunded subtracts, Failed is ignored).

Task 8 — Variant routing

Given the sealed PaymentEvent, write Describe(e PaymentEvent) string with a type switch and a default branch that panics with "unhandled payment event".

Task 9 — Sealed Option[T] (Some/None)

Build a generic Option[T] with Some[T]{Value T} and None[T]{}. Provide Get(o Option[T]) (T, bool).

Task 10 — Visitor pattern via type switch

For the sealed Expr, write Walk(e Expr, visit func(Expr)) for a depth-first pre-order traversal. Drive recursion via a type switch.

Task 11 — Counting variant kinds

Given []Expr, write Counts(es []Expr) (numbers, binops, vars int) that walks the slice and increments per-kind counters.


Hard 🔴

Task 12 — Sealed JSON value

Model JSON as a sealed JSONValue with six variants: Null{}, Bool{V bool}, Number{V float64}, String{V string}, Array{Items []JSONValue}, Object{Fields map[string]JSONValue}. Write Stringify(v JSONValue) string producing compact JSON in a single type switch.

Task 13 — ADT for HTTP routes (Match returns enum)

Model an HTTP router as a sealed ADT with Static{Path}, Param{Prefix, Name}, Wildcard{Prefix}. Write Match(r Route, path string) (kind RouteKind, rest string) where RouteKind is an enum (KindMiss, KindStatic, KindParam, KindWildcard). Return the parameter value or wildcard tail in rest.

Task 14 — Mini AST with sealed Node

Design a sealed Node for a tiny language: Lit{V int}, Add{L, R Node}, Let{Name string; Value, Body Node}, Ref{Name string}. Implement Eval(n Node, env map[string]int) int purely via a type switch. Let introduces a new lexical binding for Body only.

Task 15 — Exhaustive switch via go-vet directive comment

Add an //exhaustive:enforce directive comment so the exhaustive linter (run via go vet -vettool=$(which exhaustive)) flags missing cases. Place the comment above a type switch over a sealed interface, and document why this matters when a new variant is added.

Task 16 — Decoding JSON without leaking shapes

Extend JSONValue with Parse(raw string) (JSONValue, error). It must reject unknown shapes and never return a non-sealed implementation. Only null, bool, number, string, array, object are accepted.

Task 17 — Reducer over sealed events

Write a generic driver Fold[S any](events []SealedEvent, init S, step func(S, SealedEvent) S) S over a sealed SealedEvent interface. Then build BalanceProjection for the Task 7 payment events using Fold. The driver must be variant-agnostic, but only sealed events can be passed.


Expert 🟣

Task 18 — Migrate an open interface into sealed without breaking external users

You inherit a public Shape used across many repositories. Plan a backward-compatible migration to a sealed Shape so that existing third-party implementations keep compiling for one release cycle, new code goes through a sealed ShapeV2, and a bridge Adapt(Shape) ShapeV2 exists for old implementations.

Task 19 — Sealed Result[T] emulating Rust's Result

Implement a generic sealed Result[T any] with Ok[T]{Value T} and Err[T]{Err error}. Provide Map, AndThen, Unwrap. The variants must be sealed so no external package can introduce a third state.

Task 20 — Plugin-safe sealed interface using type identity

Design a sealed Capability that lives in a public package yet remains sealed. Use the unexported-method trick combined with type-identity rules to forbid same-named methods in another package from satisfying the interface. Show a failing example.

Task 21 — Pattern-match helper over sealed Expr

Implement Match[R any](e Expr, m Cases[R]) R where Cases[R] exposes one function per variant. Required fields (zero-valued = panic). Demonstrate that adding a new variant becomes compile-time pressure because every Cases[R] literal must be updated.


Solutions

Solution 1

type Shape interface{ isShape() }

type Circle struct{ Radius float64 }
type Square struct{ Side float64 }

func (Circle) isShape() {}
func (Square) isShape() {}

Solution 2

type Expr interface{ isExpr() }

type Number struct{ Value float64 }
type BinOp  struct{ Op rune; Left, Right Expr }
type Var    struct{ Name string }

func (Number) isExpr() {}
func (BinOp)  isExpr() {}
func (Var)    isExpr() {}

Solution 3

func Num(v float64) Expr          { return Number{Value: v} }
func Bin(op rune, l, r Expr) Expr { return BinOp{Op: op, Left: l, Right: r} }
func V(name string) Expr          { return Var{Name: name} }

Solution 4

func Print(e Expr) string {
    switch x := e.(type) {
    case Number: return strconv.FormatFloat(x.Value, 'f', -1, 64)
    case Var:    return x.Name
    case BinOp:  return "(" + Print(x.Left) + " " + string(x.Op) + " " + Print(x.Right) + ")"
    }
    panic("unreachable: sealed Expr exhausted")
}

Solution 5

type Direction interface{ isDirection() }
type North struct{}; type South struct{}; type East struct{}; type West struct{}

func (North) isDirection() {}
func (South) isDirection() {}
func (East)  isDirection() {}
func (West)  isDirection() {}

Solution 6

func Eval(e Expr, env map[string]float64) (float64, error) {
    switch x := e.(type) {
    case Number: return x.Value, nil
    case Var:
        v, ok := env[x.Name]
        if !ok { return 0, fmt.Errorf("unknown variable %q", x.Name) }
        return v, nil
    case BinOp:
        l, err := Eval(x.Left, env);  if err != nil { return 0, err }
        r, err := Eval(x.Right, env); if err != nil { return 0, err }
        switch x.Op {
        case '+': return l + r, nil
        case '-': return l - r, nil
        case '*': return l * r, nil
        case '/': return l / r, nil
        }
        return 0, fmt.Errorf("unknown op %q", x.Op)
    }
    panic("unreachable: sealed Expr exhausted")
}

Solution 7

type PaymentEvent interface{ isPaymentEvent() }
type Captured struct{ ID string; Amount int }
type Refunded struct{ ID string; Amount int }
type Failed   struct{ ID string; Reason string }

func (Captured) isPaymentEvent() {}
func (Refunded) isPaymentEvent() {}
func (Failed)   isPaymentEvent() {}

func Apply(events []PaymentEvent) (bal int) {
    for _, e := range events {
        switch x := e.(type) {
        case Captured: bal += x.Amount
        case Refunded: bal -= x.Amount
        case Failed:   // ignored
        }
    }
    return
}

Solution 8

func Describe(e PaymentEvent) string {
    switch x := e.(type) {
    case Captured: return fmt.Sprintf("captured %d on %s", x.Amount, x.ID)
    case Refunded: return fmt.Sprintf("refunded %d on %s", x.Amount, x.ID)
    case Failed:   return fmt.Sprintf("failed %s: %s", x.ID, x.Reason)
    default:       panic("unhandled payment event")
    }
}

Solution 9

type Option[T any] interface{ isOption() }
type Some[T any] struct{ Value T }
type None[T any] struct{}

func (Some[T]) isOption() {}
func (None[T]) isOption() {}

func Get[T any](o Option[T]) (T, bool) {
    if s, ok := o.(Some[T]); ok { return s.Value, true }
    var zero T
    return zero, false
}

Solution 10

func Walk(e Expr, visit func(Expr)) {
    visit(e)
    switch x := e.(type) {
    case BinOp:
        Walk(x.Left, visit)
        Walk(x.Right, visit)
    case Number, Var: // leaves
    }
}

Solution 11

func Counts(es []Expr) (numbers, binops, vars int) {
    for _, e := range es {
        switch e.(type) {
        case Number: numbers++
        case BinOp:  binops++
        case Var:    vars++
        }
    }
    return
}

Solution 12

type JSONValue interface{ isJSON() }
type Null struct{}; type Bool struct{ V bool }; type Number struct{ V float64 }
type String struct{ V string }
type Array  struct{ Items []JSONValue }
type Object struct{ Fields map[string]JSONValue }

func (Null) isJSON()   {}
func (Bool) isJSON()   {}
func (Number) isJSON() {}
func (String) isJSON() {}
func (Array) isJSON()  {}
func (Object) isJSON() {}

func Stringify(v JSONValue) string {
    switch x := v.(type) {
    case Null:   return "null"
    case Bool:   if x.V { return "true" }; return "false"
    case Number: return strconv.FormatFloat(x.V, 'f', -1, 64)
    case String: b, _ := json.Marshal(x.V); return string(b)
    case Array:
        parts := make([]string, len(x.Items))
        for i, it := range x.Items { parts[i] = Stringify(it) }
        return "[" + strings.Join(parts, ",") + "]"
    case Object:
        keys := make([]string, 0, len(x.Fields))
        for k := range x.Fields { keys = append(keys, k) }
        sort.Strings(keys)
        parts := make([]string, len(keys))
        for i, k := range keys {
            kb, _ := json.Marshal(k)
            parts[i] = string(kb) + ":" + Stringify(x.Fields[k])
        }
        return "{" + strings.Join(parts, ",") + "}"
    }
    panic("unreachable: sealed JSONValue exhausted")
}

Solution 13

type Route interface{ isRoute() }
type Static   struct{ Path string }
type Param    struct{ Prefix, Name string }
type Wildcard struct{ Prefix string }

func (Static) isRoute()   {}
func (Param) isRoute()    {}
func (Wildcard) isRoute() {}

type RouteKind int
const ( KindMiss RouteKind = iota; KindStatic; KindParam; KindWildcard )

func Match(r Route, path string) (RouteKind, string) {
    switch x := r.(type) {
    case Static:
        if x.Path == path { return KindStatic, "" }
    case Param:
        if strings.HasPrefix(path, x.Prefix) { return KindParam, strings.TrimPrefix(path, x.Prefix) }
    case Wildcard:
        if strings.HasPrefix(path, x.Prefix) { return KindWildcard, strings.TrimPrefix(path, x.Prefix) }
    }
    return KindMiss, ""
}

Solution 14

type Node interface{ isNode() }
type Lit struct{ V int }
type Add struct{ L, R Node }
type Let struct{ Name string; Value, Body Node }
type Ref struct{ Name string }

func (Lit) isNode() {}
func (Add) isNode() {}
func (Let) isNode() {}
func (Ref) isNode() {}

func Eval(n Node, env map[string]int) int {
    switch x := n.(type) {
    case Lit: return x.V
    case Add: return Eval(x.L, env) + Eval(x.R, env)
    case Ref: return env[x.Name]
    case Let:
        next := make(map[string]int, len(env)+1)
        for k, val := range env { next[k] = val }
        next[x.Name] = Eval(x.Value, env)
        return Eval(x.Body, next)
    }
    panic("unreachable: sealed Node exhausted")
}

Solution 15

// Install: go install github.com/nishanths/exhaustive/cmd/exhaustive@latest
// Run:     go vet -vettool=$(which exhaustive) ./...
// The directive below enforces exhaustiveness even when a default branch
// exists. When a new sealed variant is added, every type switch missing
// it becomes a vet error — preventing silent fall-through.

//exhaustive:enforce
func describe(e Expr) string {
    switch e.(type) {
    case Number: return "number"
    case BinOp:  return "binop"
    case Var:    return "var"
    }
    panic("unreachable")
}

Solution 16

func Parse(raw string) (JSONValue, error) {
    var any interface{}
    dec := json.NewDecoder(strings.NewReader(raw))
    dec.UseNumber()
    if err := dec.Decode(&any); err != nil { return nil, err }
    return convert(any)
}

func convert(v interface{}) (JSONValue, error) {
    switch x := v.(type) {
    case nil:    return Null{}, nil
    case bool:   return Bool{V: x}, nil
    case string: return String{V: x}, nil
    case json.Number:
        f, err := x.Float64()
        if err != nil { return nil, err }
        return Number{V: f}, nil
    case []interface{}:
        items := make([]JSONValue, len(x))
        for i, it := range x {
            jv, err := convert(it); if err != nil { return nil, err }
            items[i] = jv
        }
        return Array{Items: items}, nil
    case map[string]interface{}:
        fields := make(map[string]JSONValue, len(x))
        for k, vv := range x {
            jv, err := convert(vv); if err != nil { return nil, err }
            fields[k] = jv
        }
        return Object{Fields: fields}, nil
    }
    return nil, fmt.Errorf("unsupported JSON shape %T", v)
}

Solution 17

type SealedEvent interface{ isEvent() }
func (Captured) isEvent() {}
func (Refunded) isEvent() {}
func (Failed)   isEvent() {}

func Fold[S any](events []SealedEvent, init S, step func(S, SealedEvent) S) S {
    s := init
    for _, e := range events { s = step(s, e) }
    return s
}

func BalanceProjection(events []SealedEvent) int {
    return Fold(events, 0, func(bal int, e SealedEvent) int {
        switch x := e.(type) {
        case Captured: return bal + x.Amount
        case Refunded: return bal - x.Amount
        }
        return bal // Failed ignored
    })
}

Solution 18

// Step 1 — keep legacy open Shape exported as-is.
type Shape interface{ Area() float64 }

// Step 2 — sealed ShapeV2 in the same package.
type ShapeV2 interface {
    Area() float64
    isShapeV2() // sealing marker
}

// Step 3 — first-party sealed implementations.
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (Rect) isShapeV2()      {}

// Step 4 — bridge old to new for one release cycle.
type adapter struct{ s Shape }
func (a adapter) Area() float64 { return a.s.Area() }
func (adapter) isShapeV2()      {}
func Adapt(s Shape) ShapeV2     { return adapter{s: s} }

// Step 5 — Deprecated: use ShapeV2 directly; remove Shape in next major.

Solution 19

type Result[T any] interface{ isResult() }
type Ok[T any]  struct{ Value T }
type Err[T any] struct{ Err error }

func (Ok[T])  isResult() {}
func (Err[T]) isResult() {}

func Map[T, U any](r Result[T], f func(T) U) Result[U] {
    switch x := r.(type) {
    case Ok[T]:  return Ok[U]{Value: f(x.Value)}
    case Err[T]: return Err[U]{Err: x.Err}
    }
    panic("unreachable")
}

func AndThen[T, U any](r Result[T], f func(T) Result[U]) Result[U] {
    switch x := r.(type) {
    case Ok[T]:  return f(x.Value)
    case Err[T]: return Err[U]{Err: x.Err}
    }
    panic("unreachable")
}

func Unwrap[T any](r Result[T]) T {
    if v, ok := r.(Ok[T]); ok { return v.Value }
    panic(r.(Err[T]).Err)
}

Solution 20

// package cap
type Capability interface {
    Name() string
    isCapability() // unexported — only types in package cap can implement it
}

type Read  struct{}
type Write struct{}

func (Read)  Name() string  { return "read" }
func (Read)  isCapability() {}
func (Write) Name() string  { return "write" }
func (Write) isCapability() {}

// In another package this fails to compile:
//   type Shadow struct{}
//   func (Shadow) Name() string  { return "x" }
//   func (Shadow) isCapability() {} // ERROR — distinct method identity
// An unexported method name from another package has a different
// fully-qualified identity, so type identity blocks foreign satisfaction.

Solution 21

type Cases[R any] struct {
    Number func(Number) R
    BinOp  func(BinOp) R
    Var    func(Var) R
}

func Match[R any](e Expr, m Cases[R]) R {
    if m.Number == nil || m.BinOp == nil || m.Var == nil {
        panic("Match: all cases must be provided")
    }
    switch x := e.(type) {
    case Number: return m.Number(x)
    case BinOp:  return m.BinOp(x)
    case Var:    return m.Var(x)
    }
    panic("unreachable: sealed Expr exhausted")
}
// Adding a variant (e.g. Call) forces every Cases[R] literal to grow a
// Call field — turning omissions into compile errors at each call site.