Pattern Matching in Java (17)

Developer Advocate

Java Team at Oracle

Salvation

Salvation by Peter F. Hamilton

The Back Story

We want to evaluate simple arithemtic computations,
for example:

1 + (-2) + |3 + (-4)|

Add(1, (-2), |3 + (-4)|)

Add(1, Negate(2), |3 + (-4)|)

Add(1, Negate(2), Absolute(3 + (-4)))

Add(1, Negate(2), Absolute(Add(3, (-4))))

Add(1, Negate(2), Absolute(Add(3, Negate(4))))

The Back Story

1 + (-2) + |3 + (-4)|

Add(1, Negate(2), Absolute(Add(3, Negate(4))))

arithmetic tree

The Main Characters

interface Node { }

record Number(long number) implements Node { }

record Negate(Node node) implements Node { }

record Absolute(Node node) implements Node { }

record Add(List<Node> summands) implements Node {

	Add(Node... summands) {
		this(List.of(summands));
	}

}

The Main Characters

arithmetic tree
new Add(
	new Number(1),
	new Negate(new Number(2)),
	new Absolute(new Add(
		new Number(3),
		new Negate(new Number(4)))));

The Complication

Given a Node, evaluate the computation.

Easy:

interface Node { long evaluate(); }

record Number(long number) implements Node {
	public long evalute() { return number; }
}

record Negate(Node node) implements Node {
	public long evalute() { return -node.evalute(); }
}

The Crisis

If you change Node, we annihilate Earth!

— πŸ‘ΎπŸ‘ΎπŸ‘Ύ

We need to evaluate "from the outside":

static long evaluate(Node node) {
	// ...
}

The Plot Thickens

Evaluate Node without changing the interface.

Flashback

interface Node { }

record Number(long number) implements Node { }
record Negate(Node node) implements Node { }
record Absolute(Node node) implements Node { }
record Add(List<Node> summands) implements Node { }

Type Checking and Casting

static long evaluate(Node node) {
	if (node instanceof Number)
		return ((Number) node).number();
	if (node instanceof Negate)
		return -evaluate(((Negate) node).node());
	if (node instanceof Absolute) {
		long r = evaluate(((Absolute) node).node());
		return r >= 0 ? r : -r;
	}
	if (node instanceof Add)
		return ((Add) node)
			.summands().stream()
			.mapToLong(Nodes::evaluate)
			.sum();
	throw new IllegalStateException();
}

Type Checking and Casting

Showdown:

  • hard to write and read

  • casting is error-prone

  • new/forgotten Node implementations
    lead to run-time errors

Type Patterns

A pattern is:

  1. a test/predicate that is applied to a target

  2. pattern variables that are extracted from
    the target if the test passes

This is a type pattern:

//       |---- pattern -----|
//target |---- test -----| variable
if (node instanceof Number no)
	// ... use `Number no`

(Type pattern were finalized in Java 16.
We will see more patterns in the future.)

Type Patterns

static long evaluate(Node node) {
	if (node instanceof Number no)
		return no.number();
	if (node instanceof Negate neg)
		return -evaluate(neg.node());
	if (node instanceof Absolute abs) {
		long result = evaluate(abs.node());
		return result >= 0 ? result : -result;
	}
	if (node instanceof Add add)
		return add
			.summands().stream()
			.mapToLong(Nodes::evaluate)
			.sum();
	throw new IllegalStateException();
}

Type Patterns

Better:

  • makes writing and reading easier

  • removes error-prone casting

Pattern Matching in Switch

Allows us to also use patterns in switch:

// makes `node` the target
switch (node) {
//  |- test --| variable
	case Number no -> // ... use `Number no`
	// other cases
}

(This is a preview feature in Java 17.)

Pattern Matching in Switch

static long evaluate(Node node) {
	return switch (node) {
		case Number no -> no.number();
		case Negate neg -> -evaluate(neg.node());
		case Absolute abs -> {
			long result = evaluate(abs.node());
			yield result >= 0 ? result : -result;
		}
		case Add add -> add
			.summands().stream()
			.mapToLong(Nodes::evaluate)
			.sum();
		default ->
			throw new IllegalArgumentException();
	};
}

Pattern Matching in Switch

Better:

  • makes writing and reading even easier

Deconstruction Patterns

Allows us to deconstruct records
into their components:

// flashback
record Number(long number) implements Node { }

if (node instanceof Number(long no))
	// ... use `long no`
switch (node) {
	case Number(long no) -> // ... use `long no`
	// other cases
}

(Candidate for preview in Java 18.)

Deconstruction Patterns

static long evaluate(Node node) {
	return switch (node) {
		case Number(long no) -> no;
		case Negate(var n) -> -evaluate(n);
		case Absolute(var n) {
			long result = evaluate(n);
			yield result >= 0 ? result : -result;
		};
		case Add(var summands) -> summands.stream()
			.mapToLong(Nodes::evaluate)
			.sum();
		default ->
			throw new IllegalArgumentException();
	};
}

Deconstruction Patterns

Better:

  • reduces number of variables/calls

Guarded Patterns

Allows us to add boolean checks to patterns:

Node node = // ...
switch (node) {
	case Number(long no && no > 0) ->
		// ... use `long no`, which is positive
	case Number(long no && no < 0) ->
		// ... use `long no`, which is negative
	case Number(long __) ->
		// we know the number is 0
	// other cases
}

(This is a preview feature in Java 17.)

Guarded Patterns

static long evaluate(Node node) {
	return switch (node) {
		case Number(long no) -> no;
		case Negate(var n) -> -evaluate(n);
		case Absolute(var n) && evaluate(n) < 0 ->
			-evaluate(n);
		case Absolute(var n) -> evaluate(n);
		case Add(var summands) -> summands.stream()
			.mapToLong(Nodes::evaluate)
			.sum();
		default ->
			throw new IllegalArgumentException();
	};
}

Guarded Patterns

Better:

  • elevates checks into case

Sealed Classes

Controls inheritance and
makes inheriting types known:

// πŸ‘ΎπŸ‘ΎπŸ‘Ύ: 🀨
sealed interface Node
	permits Number, Negate, Absolute, Add { }
// ⇝ no other type can implement `Node`

(Sealed classes were finalized in Java 17.)

Sealed Classes

static long evaluate(Node node) {
	return switch (node) {
		case Number(long no) -> no;
		case Negate(var n) -> -evaluate(n);
		case Absolute(var n) && evaluate(n) < 0
			-> -evaluate(n);
		case Absolute(var n) -> evaluate(n);
		case Add(var summands) -> summands.stream()
			.mapToLong(Nodes::evaluate)
			.sum();
		// no default branch needed
	};
}

Sealed Classes

Better:

  • turns new/forgotten Node implementations
    into compile errors

Timeline

Java 16
  • type patterns

Java 17
  • sealed classes

  • patterns in switch (preview)

  • guarded patterns (preview)

Java ??
  • deconstruction patterns
    (candidate for preview in 18)

Plot Holes

  • pattern variable scope

  • dominance of pattern labels

  • completeness of pattern labels

  • null handling in switch

  • rules for sealed types

  • deconstruction of arrays

Happy End

if-else chains over types and conditions are:

  • hard to write and read

  • error-prone due to casting

  • hard to maintain when type hierarchy expands

Compare to pattern matching:

  • easy to write and read (after getting used to them)

  • safe casting and easy deconstructing out of the box

  • sealed types allow use-site opt-in compiler
    support for expanding type hierarchy

Resolution

When should you use this?

Domain Overload

Should you add evaluate this way?
Probably not.

But what about:

  • Resources estimateResourceUsage()

  • Strategy createComputationStrategy()

  • Invoice createInvoice(User u)

  • String prettyPrint() (like here)

  • void draw(Direction d, Style s, Canvas c)

⇝ Central abstractions can be overburdened
with diverse requirements.

Visitor Pattern

Separating a hierarchy from operations
is a case for the visitor pattern.

πŸ‘ΎπŸ‘ΎπŸ‘Ύ

Using pattern matching over sealed type:

  • will often achieve same results

  • leads to much simpler code

Edge of Space

When parsing outside data,
types are often general
(think JsonNode).

Consider pattern matching
to tease apart the data.

Conclusion

  • polymorphism remains default approach
    to adding operations to types

  • where that’s not ideal or where types are general
    consider pattern matching (and sealed classes)

  • together, they make it easy and safe
    to branch based on type and other properties

Sequels

So long…​

37% off with
code fccparlog

bit.ly/the-jms

More

Slides at slides.nipafx.dev
β‡œ Get my book!

Follow Nicolai

nipafx.dev
/nipafx

Follow Java

inside.java
/java    //    /openjdk

Image Credits