Typeclasses In Depth
haskell1-8 introduced typeclasses through the standard trio. This chapter writes real, multi-method typeclasses from scratch, and gets specific about where Haskell's version genuinely diverges from rust2-2's own traits — not just naming the shared lineage again, but the real differences too.
Beyond Eq/Ord/Show — A Real Custom Typeclass
A genuinely useful multi-method typeclass — every instance must supply both area and perimeter.
Default Method Implementations
A typeclass method can carry a default body — the same direct genetic connection haskell1-8 already named between typeclasses and both rust2-2's trait default methods and java1-6's Java 8 interface defaults. An instance can override describe, or simply inherit the default.
Minimal Complete Definitions
A genuinely nice, practical Haskell-specific idiom: the {-# MINIMAL #-} pragma tells instance authors exactly which subset of methods actually needs a real implementation, with the rest derivable from those. Not present in quite this form in Rust's, Java's, or Kotlin's own interface/trait systems.
Superclass Constraints — Building Real Hierarchies
Already used implicitly since haskell2-2's own Applicative/Monad hierarchy — a typeclass can require another as a prerequisite, comparable to rust2-2's own supertrait bounds and interface inheritance in Java/Kotlin/C#.
Rust Traits vs. Haskell Typeclasses — The Real Differences
Not just "same idea, different name" — a few genuine, concrete differences worth naming honestly. Rust's orphan rule generally requires either the trait or the type to be defined in your own crate to write an impl; Haskell's typeclass system has historically been more permissive by default, though GHC extensions exist to tighten or loosen this in different scenarios. Rust also distinguishes dyn Trait (dynamic dispatch) from generic/impl Trait (static dispatch) as two separate mechanisms a programmer chooses between explicitly — haskell1-8's own dictionary-passing dispatch is more uniform, working the same way underneath regardless of how static- or dynamic-feeling the usage looks.
Multi-Parameter Type Classes, Briefly
A real, honest mention: a typeclass can be parameterized over more than one type at once, but this genuinely requires a GHC extension — it's not part of standard Haskell 2010. Flagged plainly as an extension, not core language, the same honest-wart spirit as earlier chapters' own acknowledgments.
| Aspect | Rust traits (rust2-2) | Java/Kotlin interfaces (java1-6) | Haskell typeclasses |
|---|---|---|---|
| Default methods | yes | yes (Java 8+) | yes |
| Superclass/supertrait constraints | yes | yes (interface extends) | yes |
| Implementing for types outside your own code | restricted (orphan rule) | no | more permissive by default |
| Static vs. dynamic dispatch | explicit choice (dyn Trait vs generics) | vtable, always | dictionary passing, uniform |
| Multiple type parameters | supported directly | generic interfaces | needs a GHC extension |
== and /= as something other than logical negations of each other — nothing at compile time catches this. A real, genuine correctness risk resting purely on the instance author's own discipline, the same honest-limitation spirit as the unenforced Functor and Monad laws from earlier chapters.
Coding Challenges
Define a Shape typeclass with area and perimeter methods, plus a describe default method, and write instances for two different shapes, calling describe on both without overriding it.
📄 View solutionExtend Challenge 1's setup with a Shape3D typeclass requiring Shape as a superclass constraint, adding a volume method, and write one instance for a genuine 3D shape.
📄 View solutionWrite a short comment explaining the real difference between Rust's dyn Trait/generic split and Haskell's own uniform dictionary-passing dispatch, referencing haskell1-8's own dispatch material directly.
📄 View solutionChapter 5 Quick Reference
- A typeclass can require multiple methods, with default bodies available for some — the same rust2-2/java1-6 lineage haskell1-8 already named
- {-# MINIMAL #-} documents exactly which subset of methods an instance must genuinely implement
- Superclass constraints (class Shape a => Shape3D a) build real hierarchies, the same pattern Applicative/Monad already used
- Real differences from Rust traits: the orphan rule vs. Haskell's more permissive defaults, and Rust's explicit dyn/generic dispatch choice vs. Haskell's uniform dictionary passing
- Multi-parameter type classes are a real GHC extension, not core Haskell 2010
- Nothing enforces logical consistency between related methods in one instance — a real, honest correctness risk
- Next chapter: monad transformers — stacking effects, and an honest look at where the elegance gets harder