Dec Code Story

Chapter #9 – Type Classes & Interfaces

ad-hoc polymorphism, instance derivation, higher-kinded types, Functor/Applicative/Monad

9.0  Prologue

A type class (Haskell) or trait (Rust) declares a set of operations that multiple types can implement independently. This enables ad-hoc polymorphism: writing generic code that works for any type satisfying the required interface, with the specific implementation chosen automatically by the compiler based on the type at the call site.

9.1  Ad-Hoc Polymorphism

Ad-hoc polymorphism allows the same function name to behave differently for different types, with each type providing its own implementation. -- Haskell: type class declaration and instances class Describable a where describe :: a -> String data Color = Red | Green | Blue instance Describable Color where describe Red = "red" describe Green = "green" describe Blue = "blue" instance Describable Int where describe n = "the number " ++ show n -- Generic function works for any Describable type printDescription :: Describable a => a -> IO () printDescription x = putStrLn (describe x) -- Rust: trait (same concept) trait Describable { fn describe(&self) -> String; } fn print_description(x: &impl Describable) { println!("{}", x.describe()); } The key difference from object-oriented interfaces: type class instances are resolved globally by the compiler (coherence), not dispatched through a vtable at runtime, enabling zero-cost static dispatch.

9.2  Instance Derivation

Deriving automatically generates type class instances for common operations. The compiler inspects the structure of the type and produces the implementation mechanically. -- Haskell: deriving generates instances automatically data Point = Point { x :: Double, y :: Double } deriving (Show, Eq, Ord, Read, Generic) -- Show: show (Point 1 2) => "Point {x = 1.0, y = 2.0}" -- Eq: Point 1 2 == Point 1 2 => True -- Ord: comparison by field order -- Rust: derive macro generates trait impls #[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] struct Point { x: f64, y: f64 } // Debug: println!("{:?}", point) // Clone: let p2 = point.clone() // PartialEq: point == other -- F#: structural equality and comparison are derived by default for records type Point = { X: float; Y: float } { X=1.0; Y=2.0 } = { X=1.0; Y=2.0 } // true

9.3  Higher-Kinded Types

A higher-kinded type (HKT) is a type that takes another type as a parameter. Maybe and List are not types - they are type constructors of kind * -> *. Type classes over HKTs abstract over the container shape, not the contained type. -- Haskell: Functor abstracts over any container f that supports fmap class Functor f where fmap :: (a -> b) -> f a -> f b -- The same fmap works for Maybe, [], Either e, IO, ... fmap (+1) (Just 5) -- Just 6 fmap (*2) [1,2,3] -- [2,4,6] fmap toUpper "hello" -- "HELLO" -- Rust: HKT not natively supported; approximated via GATs or associated types // The Iterator trait is effectively a HKT abstraction over sequence shape HKTs are the reason Haskell can express Functor, Applicative, and Monad as type classes that apply to any container or effect shape. Without HKTs, each container needs its own specialized map.

9.4  Functor / Applicative / Monad Hierarchy

The three core abstractions for effectful computation form a hierarchy: each extends the previous with additional power.
Class Key operation What it adds
Functor fmap :: (a->b) -> f a -> f b Map a pure function over a context
Applicative (<*>) :: f (a->b) -> f a -> f b Apply a wrapped function to a wrapped value
Monad (>>=) :: m a -> (a -> m b) -> m b Sequence: result of first computation affects second
-- Functor: transform a value inside a context (no new context creation) fmap (*2) (Just 5) -- Just 10 fmap (*2) Nothing -- Nothing -- Applicative: apply, combine multiple independent contexts pure (+) <*> Just 3 <*> Just 4 -- Just 7 -- Monad: sequence where second step depends on result of first Just 5 >>= \x -> if x > 0 then Just (x*2) else Nothing -- Just 10 Nothing >>= \x -> Just (x*2) -- Nothing These three classes appear in Rust as the Iterator, Option, and Result combinators (map, zip, and_then). The Haskell type classes make the pattern explicit and uniform across all types that satisfy the laws.

9.5  Epilogue

Type classes provide a principled vocabulary for abstraction: Eq, Ord, Show, Functor, Monad are not ad-hoc naming conventions but formal interfaces with mathematical laws that instances must satisfy. Code written against these interfaces is correct for any conforming type, now and in the future. The next chapter examines how immutability shapes the data structures declarative programs rely on.

9.6  References

Haskell Type Classes - Tutorial
Typeclassopedia - Haskell Wiki
Rust Traits - The Book
Type Class - Wikipedia