Typeclasses In Depth

Course 2 · Ch 5
Typeclasses In Depth
Beyond Eq/Ord/Show — and the real, honest differences from rust2-2's own trait system

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

class Shape a where area :: a -> Double perimeter :: a -> Double instance Shape Circle where area (Circle r) = pi * r * r perimeter (Circle r) = 2 * pi * r

A genuinely useful multi-method typeclass — every instance must supply both area and perimeter.

Default Method Implementations

class Shape a where area :: a -> Double perimeter :: a -> Double describe :: a -> String describe s = "Area: " ++ show (area s) -- a real default body

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

class MyEq a where {-# MINIMAL (==) | (/=) #-} -- only ONE of these strictly needs a real implementation (==), (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)

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

class Shape a => Shape3D a where volume :: a -> Double -- Shape3D REQUIRES Shape first — the same pattern haskell2-2's Monad required Applicative

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

{-# LANGUAGE MultiParamTypeClasses #-} class Convert a b where convert :: a -> b

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.

AspectRust traits (rust2-2)Java/Kotlin interfaces (java1-6)Haskell typeclasses
Default methodsyesyes (Java 8+)yes
Superclass/supertrait constraintsyesyes (interface extends)yes
Implementing for types outside your own coderestricted (orphan rule)nomore permissive by default
Static vs. dynamic dispatchexplicit choice (dyn Trait vs generics)vtable, alwaysdictionary passing, uniform
Multiple type parameterssupported directlygeneric interfacesneeds a GHC extension
Use {-# MINIMAL #-} when your own typeclass has derivable methods
Documenting the true minimal set an instance author must supply — with everything else provided as a sensible default — keeps a custom typeclass genuinely easy to implement correctly.
Nothing stops an instance from disagreeing with itself
An instance could technically define == 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

Challenge 1

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 solution
Challenge 2

Extend 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 solution
Challenge 3

Write 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 solution

Chapter 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