λ fbounded  :: Blog
All posts
#java #types #functional-programming #algebras

Algebraic Types: The Math Hiding in Your Java Code

Product types, sum types, and why combining them gives you a small, complete algebra — explained through Java records and sealed interfaces. First in a series leading to algebraic effects.

You Already Write Algebras

Most Java developers have written something like this:

record Point(double x, double y) {}

And something like this:

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius)               implements Shape {}
record Rectangle(double width, double height) implements Shape {}

These aren’t just convenient syntax. They represent two fundamental kinds of types that, when combined, give you something mathematicians call an algebra. That word sounds intimidating, but the idea is simple — and once you see it, you’ll recognise the pattern everywhere.


Product Types: The AND

A record Point(double x, double y) holds an x and a y. Every Point has both — you can’t have one without the other.

This is called a product type because the number of possible values is the product of the possibilities of each field.

A boolean has 2 possible values. A record Pair(boolean a, boolean b) has 2 × 2 = 4:

  • (false, false)
  • (false, true)
  • (true, false)
  • (true, true)

The name comes from multiplication. Any class or record that bundles fields together is a product type:

record Person(String name, int age) {}        // String AND int
record Circle(Point center, double radius) {} // Point AND double

Java has always had product types — every class with multiple fields is one. They’ve just never been called that.


Sum Types: The OR

A Shape is either a Circle or a Rectangle — never both, never neither. This is a sum type (also called a tagged union or variant): a value that is exactly one of a fixed set of alternatives.

The name follows the same logic: if Circle has N possible values and Rectangle has M possible values, then Shape has N + M possible values — the sum.

Java’s sealed interfaces (introduced in Java 17, finalised in Java 21) let you express this directly:

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius)               implements Shape {}
record Rectangle(double width, double height) implements Shape {}

Before sealed interfaces, Java developers reached for enums as a limited substitute:

enum Direction { NORTH, SOUTH, EAST, WEST }

An enum with four variants has exactly four possible values — a sum type. Sealed interfaces extend the idea: each variant can carry its own data, and that data can differ between variants.


Putting Them Together: An Algebra

“Algebra” in everyday math means symbols and rules — x + y, 2 × 3. In type theory it means something slightly more general: a set of types together with the operations you can perform on them.

Sum types and product types are the vocabulary. Functions that consume or produce those types are the grammar.

Consider a tiny expression language:

sealed interface Expr permits Num, Add, Mul {}
record Num(int value)              implements Expr {}
record Add(Expr left, Expr right)  implements Expr {}
record Mul(Expr left, Expr right)  implements Expr {}

Add and Mul are both product types (they hold a left and a right sub-expression) and alternatives in the sum type Expr. You can build expressions by composing constructors:

// Represents: (2 + 3) * 4
Expr e = new Mul(
    new Add(new Num(2), new Num(3)),
    new Num(4)
);

Now add an evaluator:

static int eval(Expr expr) {
    return switch (expr) {
        case Num(var v)           -> v;
        case Add(var l, var r)    -> eval(l) + eval(r);
        case Mul(var l, var r)    -> eval(l) * eval(r);
    };
}

This is an algebra:

  • The type (Expr) is the set of values the algebra works over.
  • The constructors (Num, Add, Mul) are the ways to build those values.
  • The operation (eval) is something you can do with any value in the set.

The algebra is closed: every Expr can be evaluated, and every eval call returns an int. Nothing falls through. The compiler enforces completeness — the switch is exhaustive because Expr is sealed.


Laws: What the Algebra Guarantees

Types and operations alone don’t make a useful algebra — the operations need to satisfy laws: equations that must hold for any inputs.

For eval over integer addition, those laws are:

// Identity — adding zero changes nothing
eval(new Add(new Num(0), e)) == eval(e)
eval(new Add(e, new Num(0))) == eval(e)

// Associativity — grouping doesn't change the result
eval(new Add(new Add(a, b), c)) == eval(new Add(a, new Add(b, c)))

// Commutativity — order doesn't matter
eval(new Add(a, b)) == eval(new Add(b, a))

These aren’t just nice properties. They’re what lets you safely simplify x + 0x, or reorder additions for parallel evaluation. The laws tell you exactly which rewrites are safe.

Laws also give you a concrete testing target. Instead of checking specific values, you can write property-based tests that assert a law holds for arbitrary inputs — if a refactoring breaks a law, a test catches it.

Not every algebra shares these laws. String concatenation is associative (("a"+"b")+"c" == "a"+("b"+"c")) but not commutative ("ab"+"c""c"+"ab"). A Result-chaining operation has identity laws but no commutativity. Different algebras, different guarantees — knowing the laws is knowing what you can and can’t assume.


Why This Matters

Structuring your domain as an algebra gives you concrete, practical benefits.

Exhaustiveness checking. When you pattern-match on a sealed type, the compiler tells you if you’ve missed a case. Add a new variant and every switch that doesn’t handle it becomes a compile error — before you ship.

No invalid states. Compare two ways to model a result type:

// Before: three fields, undefined combinations
record Result(String value, String error, boolean success) {}
// What does {null, null, true} mean?
// What does {"ok", "also an error", true} mean?
// After: an algebra — impossible states are unrepresentable
sealed interface Result<T> permits Ok, Err {}
record Ok<T>(T value)     implements Result<T> {}
record Err<T>(String error) implements Result<T> {}

There are no invalid combinations. Ok always has a value. Err always has an error. The type system enforces the invariant so your code doesn’t have to.

Composability. Small algebras compose into larger ones. Expr uses int as its output. You could swap eval for a prettyPrint that returns String, or a typeCheck that returns a Type — a different algebra altogether — without changing Expr at all.


The Connection to Functional Programming

If you’ve encountered Haskell, Scala, or F#, this pattern will look familiar. Those languages call the combination of sum and product types algebraic data types (ADTs), and they’re central to how functional programs model their domains.

Java didn’t have good sum types until sealed classes landed in JDK 21. That’s one reason functional patterns felt awkward in Java for a long time. With sealed interface + record + exhaustive pattern matching, Java finally has the pieces — the concepts just weren’t part of the standard Java vocabulary.

The terminology — product, sum, algebra — comes from category theory, which is the branch of mathematics that underlies a lot of functional programming. You don’t need to know category theory to use these ideas. But knowing the names helps: you can search for patterns, recognise them in other languages, and reason about them more precisely.


Where This Is Going

Once you’re comfortable thinking in algebras — types as sets, constructors as vocabulary, functions as operations — a natural next question is: what about effects?

eval is pure: it takes an Expr and returns an int, nothing more. But real programs log things, read files, throw exceptions, run concurrently. How do you model those in an algebra? How do you keep the composability and exhaustiveness guarantees when your operations have side effects?

That’s the territory of algebraic effects — a model for structured, composable side effects that’s making its way into modern languages and runtimes. It’s the subject of the next post in this series.

For now: if you can write a sealed interface and a record, you can already think in algebras. You just didn’t have a name for it.