A type class defines operations that a type must support. When a type has an instance of that class, client code can rely on those operations without exposing the type’s representation or selecting a concrete implementation directly. This makes a type class a functional way to express an interface and add shared behavior to user-defined types. section 4.3 section 1.3.3
Use a type class when code needs a capability rather than a particular data constructor. A `WithHandler handlerAPI` class, for example, supplies `withHandler`, which accepts a `DevicePart` and a callback requiring a handler. The caller requests the needed API through the callback instead of pattern matching through vendor-specific internals. section 4.2.5
This shifts responsibilities in a useful way:
- Client code names the required handler API in its callback, such as `SensorAPI -> IO ()`, rather than extracting a handler from `DevicePart`. section 4.2.5
- Instances decide how a supported `DevicePart` yields a `SensorAPI` or `ControllerAPI` handler. section 4.2.5
- The public service interface remains the normal boundary; bypassing it to inspect internal constructors is a design flaw. section 4.2.5 section 4.3
Selecting a database at the type level
A database class can associate a phantom type with a database name. Distinct empty types such as `ValuesDB` and `AstroDB` receive separate `DB` instances. The key–value language carries `db` in `KVDBL db`, so a call can identify its intended database through a type application like `load "key1" @ValuesDB` or through an explicit result type. The database connection remains a separate `DBHandle db` dependency. section 11.1.2 section 11.1.2
Type classes can also select service implementations. A final tagless/mtl interface constrains business logic with `AstroService api`; type selectors choose HTTP or TCP instances at composition time. This can look like dependency injection, but carrying channel details and sharing an environment between business logic and instances can leave the layers tightly coupled. Type classes are therefore an option to assess against algebraic-data-type interpreters and free-monad interfaces, not an automatic architectural improvement. section 13.2.6 section 1.3.3
For broader type-level design, type classes provide limited expressiveness. Functional dependencies, associated types, type families, existential types, and generalized ADTs extend the available tools; dependent types are identified as a more powerful direction. section 11.2.2 section 11.3
-- module Andromeda.Hardware.Impl.Device.Types: instance WithHandler SensorAPI where withHandler (DevicePart (VendoredSensor _ h)) f = f h withHandler _ _ = error "Invalid part API handler" instance WithHandler ControllerAPI where withHandler (DevicePart (VendoredController _ h)) f = f h withHandler _ _ = error "Invalid part API handler"
Compare the two `WithHandler` instances to see how the same interface exposes different handler APIs while keeping constructor matching inside the instance. section 4.2.5


