Foundations: Type Classes
"Quality is not a thing. It is an event."
– Robert M. Pirsig, Zen and the Art of Motorcycle Maintenance
While type classes power the library internally, you rarely interact with them directly. The Effect Path API exposes these capabilities through a unified interface:
| Type Class | Effect Path Method |
|---|---|
Functor.map | path.map(f) |
Monad.flatMap | path.via(f) |
MonadError.handleErrorWith | path.recover(f) |
Applicative.ap | path.ap(otherPath) |
Read this chapter to understand the foundations. Use Effect Paths for everyday programming.
A type class is not a thing you can point to. It is not an object in memory, nor a concrete class you instantiate. It is, rather, an event: the moment when a type demonstrates that it possesses certain capabilities.
When we say Optional is a Functor, we mean that mapping over an Optional is a meaningful operation. When we say it is also an Applicative, we add the ability to combine independent Optional values. When we say it is a Monad, we gain sequencing, the power to chain operations where each step depends on the previous result. The type hasn't changed. Our understanding of what we can do with it has.
This chapter presents the type class hierarchy that powers Higher-Kinded-J. At the foundation sits Functor, providing map. Above it, Applicative adds of and ap. Monad contributes flatMap. MonadError handles failure. Each builds on what came before, each unlocking new compositional possibilities.
The hierarchy is not arbitrary. It reflects mathematical structure (specifically, category theory), though you need not understand the mathematics to use the tools. Think of it as a ladder: each rung grants new capabilities, and the lower rungs remain available as you climb.
The Hierarchy
┌──────────┐
│ Functor │ map
└────┬─────┘
│
┌────┴─────┐
│Applicative│ of, ap
└────┬─────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Monad │ │Alternative│ │ Selective │
│ flatMap │ │ orElse │ │ select │
└─────┬─────┘ └───────────┘ └───────────┘
│
┌─────┴─────┐
│MonadError │ raiseError, handleErrorWith
└───────────┘
Each arrow represents "extends": Monad is an Applicative with additional power.
Why This Matters
The practical benefit is polymorphism over behaviour, not just data:
// This method works with ANY Monad: Optional, Either, List, IO, Future...
public static <F, A, B> Kind<F, B> transform(
Monad<F> monad,
Kind<F, A> value,
Function<A, Kind<F, B>> operation) {
return monad.flatMap(operation, value);
}
Write once. Use everywhere the capability exists.
What You'll Learn
- Functor – The foundational type class that enables transformation of values inside containers without changing the container's structure. Every other abstraction builds on this.
- Applicative – When you have multiple independent computations and need to combine their results. Unlike Monad, Applicative allows parallel evaluation since results don't depend on each other.
- Alternative – Provides choice and fallback semantics: try one computation, and if it fails, try another. Essential for parsing and error recovery patterns.
- Monad – The power to sequence dependent computations where each step can use results from previous steps. The workhorse of functional programming.
- MonadError – Extends Monad with explicit error handling capabilities, allowing you to raise errors and recover from them within the monadic context.
- Semigroup and Monoid – Type classes for combining values associatively. Monoids add an identity element, enabling operations like folding empty collections.
- Foldable and Traverse – Foldable lets you reduce structures to single values; Traverse lets you transform structures while collecting effects.
- MonadZero – Adds filtering capabilities to monads, enabling guard conditions in for-comprehensions that can short-circuit computation.
- Selective – Sits between Applicative and Monad, providing conditional effects that can be statically analysed. Useful for build systems and optimisation.
- Profunctor – For types with both input and output, allowing you to transform both sides. The foundation for advanced optics.
- Bifunctor – Like Functor, but for types with two type parameters. Enables simultaneous transformation of both sides of types like Either or Tuple.
- For Comprehension – A fluent API for composing monadic operations, inspired by Scala's for-comprehensions and Haskell's do-notation.
Chapter Contents
- Functional API - Overview of all type class interfaces
- Functor - The foundation: mapping over wrapped values
- Applicative - Combining independent computations
- Alternative - Choice and fallback operations
- Monad - Sequencing dependent computations
- MonadError - Explicit error handling in monadic contexts
- Semigroup and Monoid - Combining values associatively
- Foldable and Traverse - Iterating and transforming structures
- MonadZero - Filtering in comprehensions
- Selective - Conditional effects with static analysis
- Profunctor - Transforming both input and output
- Bifunctor - Mapping over two type parameters
- Natural Transformation - Polymorphic functions between type constructors
- For Comprehension - Readable monadic composition
Next: Functional API