rgoussu@goussu: ~/library/applicative-architecture/exercises
~/library/applicative-architecture/exercises cat variance-workout.md

Variance workout — LSP & ISP through the type checker

# A six-part polyglot workout making variance tangible — break array covariance, expose Rectangle/Square, drill PECS, earn Kotlin's in/out by segregating a fat interface, tighten strictFunctionTypes, finish with LSP as contract tests.

Exercisesaved 2026-08-08 #exercise#variance#covariance#contravariance#solid#liskov#interface-segregation#type-systems

Goal

Turn covariance and contravariance from vocabulary into reflexes by making the type checker referee every claim: break the unsound cases at runtime, then rebuild them so the same mistake fails to compile. Along the way, feel the two SOLID ties first-hand — Liskov substitution as the reason for the variance rules, interface segregation as the price of declaration-site out/in.

Subject: full brief & instructions

Practices

  • Covariance & contravariance — the whole exercise: producers covariant, consumers contravariant, read-write invariant.
  • Clean code — LSP and ISP stop being slide material; each part is one of them enforced by a compiler.
  • Testing strategies — property tests expose the LSP break; contract tests make substitutability executable.
  • Deep dive Java — wildcards, PECS, and the array legacy.
  • Deep dive TypeScript — function-type variance and the method-bivariance loophole.

Milestones

  1. Break arrays (Java). Write the classic Animal[] a = new Cat[1]; a[0] = new Dog(); and watch ArrayStoreException at runtime; rewrite with List<Cat> and watch the same mistake refuse to compile. One commit, two files, the runtime→compile-time move stated in a sentence.
  2. Rectangle/Square under property tests. Implement the naive Square extends Rectangle, write the property (setWidth(w)area == w × height) for any rectangle, watch Square fail it, then fix by redesign — immutable values or separate types — until the property holds for every member of the hierarchy.
  3. PECS drill (Java). Implement copy(src, dst), max(collection, comparator), and addAll(target, elements…) with wildcards against a prepared harness of should-compile and must-not-compile call sites.
  4. The ISP→variance kata (Java → Kotlin). Take a fat MultiFunctionDevice (print + scan + fax), segregate it into role interfaces, port to Kotlin, and annotate producers out / consumers in. Then try to annotate the original fat interface and keep the compiler's refusal as the artifact: the segregation is what made variance legal.
  5. strictFunctionTypes koans (TypeScript). A single file of function-assignability assertions; flip the flag, explain every new error in a comment, and document the method-bivariance loophole with one case that still slips through.
  6. LSP as contract tests. Write one abstract contract-test class for a small interface (a repository port works well), run it against every implementation including a deliberately broken one — the broken one failing the contract is the point.

Stretch goals

  • Port part 4 to Scala (+T/-T) or C# (out/in) and compare the dialects.
  • Work through the variance-flavored puzzles in type-challenges after part 5.
  • Archaeology: find one real bivariance-era bug report in the TypeScript issue tracker and trace why strictFunctionTypes couldn't fix methods without breaking the DOM types.

Related