Add sealed module: modelling domains with sealed types and exhaustive switch
Payment model, permits and the three modifiers, default-branch trap, generic and recursive hierarchies, visitor comparison, run-time enforcement, modules. Transcripts from JDK 21, 25 and 27. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
public class ExprDemo {
|
||||
sealed interface Expr permits Num, Add, Mul, Neg {}
|
||||
record Num(int value) implements Expr {}
|
||||
record Add(Expr left, Expr right) implements Expr {}
|
||||
record Mul(Expr left, Expr right) implements Expr {}
|
||||
record Neg(Expr inner) implements Expr {}
|
||||
|
||||
static int eval(Expr e) {
|
||||
return switch (e) {
|
||||
case Num(int v) -> v;
|
||||
case Add(Expr l, Expr r) -> eval(l) + eval(r);
|
||||
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
|
||||
case Neg(Expr i) -> -eval(i);
|
||||
};
|
||||
}
|
||||
|
||||
// Nested record patterns look two levels deep in one case.
|
||||
static Expr simplify(Expr e) {
|
||||
return switch (e) {
|
||||
case Add(Num(int a), Num(int b)) -> new Num(a + b);
|
||||
case Mul(Num(int a), Expr r) when a == 1 -> simplify(r);
|
||||
case Neg(Neg(Expr inner)) -> simplify(inner);
|
||||
case Add(Expr l, Expr r) -> new Add(simplify(l), simplify(r));
|
||||
case Mul(Expr l, Expr r) -> new Mul(simplify(l), simplify(r));
|
||||
case Neg(Expr i) -> new Neg(simplify(i));
|
||||
case Num n -> n;
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Expr e = new Add(new Mul(new Num(1), new Num(7)), new Neg(new Neg(new Num(3))));
|
||||
System.out.println(e);
|
||||
System.out.println("eval = " + eval(e));
|
||||
Expr s = simplify(e);
|
||||
System.out.println("simplify = " + s + " (eval " + eval(s) + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import java.util.Arrays;
|
||||
|
||||
// If the subclasses live in the same file, the permits clause can be left out: javac infers it.
|
||||
public sealed class ImplicitPermits {
|
||||
static final class A extends ImplicitPermits {}
|
||||
static final class B extends ImplicitPermits {}
|
||||
public static void main(String[] args) {
|
||||
System.out.println(Arrays.stream(ImplicitPermits.class.getPermittedSubclasses()).map(Class::getName).toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
public class PaymentDemo {
|
||||
// A Payment is exactly one of these three things, and nothing else.
|
||||
sealed interface Payment permits Card, Upi, NetBanking {}
|
||||
record Card(String last4, long amount) implements Payment {}
|
||||
record Upi(String vpa, long amount) implements Payment {}
|
||||
record NetBanking(String bank, long amount) implements Payment {}
|
||||
|
||||
// No default branch: the compiler proves every permitted type is handled.
|
||||
static long fee(Payment p) {
|
||||
return switch (p) {
|
||||
case Card(String last4, long amount) -> amount * 2 / 100; // 2 percent
|
||||
case Upi(String vpa, long amount) -> 0; // free
|
||||
case NetBanking(String bank, long amount) -> 15; // flat fee
|
||||
};
|
||||
}
|
||||
|
||||
static String describe(Payment p) {
|
||||
return switch (p) {
|
||||
case Card(String last4, long amount) when amount >= 10_000 -> "large card payment ending " + last4;
|
||||
case Card(String last4, long amount) -> "card payment ending " + last4;
|
||||
case Upi(String vpa, long amount) -> "UPI payment from " + vpa;
|
||||
case NetBanking(String bank, long amount) -> "net banking via " + bank;
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Payment[] all = {
|
||||
new Card("4242", 500), new Card("1111", 25_000),
|
||||
new Upi("ankur@bank", 500), new NetBanking("HDFC", 500)
|
||||
};
|
||||
for (Payment p : all) {
|
||||
System.out.println(p + " -> fee " + fee(p) + " | " + describe(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Reflect {
|
||||
sealed interface Shape permits Circle, Poly, Custom {}
|
||||
record Circle(double r) implements Shape {} // records are implicitly final
|
||||
sealed interface Poly extends Shape permits Tri, Quad {} // sealed again: the hierarchy can be deeper
|
||||
record Tri(double b, double h) implements Poly {}
|
||||
static final class Quad implements Poly {} // an explicit final class
|
||||
non-sealed interface Custom extends Shape {} // re-opens the hierarchy below this point
|
||||
static class Hexagon implements Custom {} // legal, and not in any permits clause
|
||||
|
||||
static void show(Class<?> c) {
|
||||
String kind = c.isSealed() ? "sealed" : Modifier.isFinal(c.getModifiers()) ? "final" : "open (non-sealed or plain)";
|
||||
System.out.println(c.getSimpleName() + ": " + kind
|
||||
+ (c.isSealed() ? ", permits " + Arrays.stream(c.getPermittedSubclasses()).map(Class::getSimpleName).toList() : ""));
|
||||
}
|
||||
|
||||
// Custom is covered by one case; Hexagon and any future subtype of Custom are covered by it too.
|
||||
static String name(Shape s) {
|
||||
return switch (s) {
|
||||
case Circle c -> "circle";
|
||||
case Tri t -> "triangle";
|
||||
case Quad q -> "quad";
|
||||
case Custom c -> "custom:" + c.getClass().getSimpleName();
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (Class<?> c : new Class<?>[]{Shape.class, Circle.class, Poly.class, Tri.class, Quad.class, Custom.class, Hexagon.class}) show(c);
|
||||
System.out.println(name(new Circle(1)) + " " + name(new Tri(1, 2)) + " " + name(new Quad()) + " " + name(new Hexagon()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ResultDemo {
|
||||
// A generic sealed type: a Result<T> is either an Ok holding a T or an Err holding a message.
|
||||
sealed interface Result<T> permits Ok, Err {}
|
||||
record Ok<T>(T value) implements Result<T> {}
|
||||
record Err<T>(String message) implements Result<T> {}
|
||||
|
||||
static <T, R> Result<R> map(Result<T> r, Function<T, R> f) {
|
||||
return switch (r) {
|
||||
case Ok<T>(T value) -> new Ok<>(f.apply(value));
|
||||
case Err<T>(String message) -> new Err<>(message);
|
||||
};
|
||||
}
|
||||
|
||||
static Result<Integer> parse(String s) {
|
||||
try { return new Ok<>(Integer.parseInt(s)); }
|
||||
catch (NumberFormatException e) { return new Err<>("not a number: " + s); }
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (String s : new String[]{"21", "abc"}) {
|
||||
Result<Integer> doubled = map(parse(s), n -> n * 2);
|
||||
String text = switch (doubled) {
|
||||
case Ok<Integer>(Integer v) -> "ok " + v;
|
||||
case Err<Integer>(String m) -> "error: " + m;
|
||||
};
|
||||
System.out.println(s + " -> " + text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
public class VisitorVersion {
|
||||
// The pre-sealed way: a visitor interface, an accept method on every type, and one implementation per operation.
|
||||
interface Payment { <R> R accept(Visitor<R> v); }
|
||||
interface Visitor<R> { R visitCard(Card c); R visitUpi(Upi u); R visitNetBanking(NetBanking n); }
|
||||
record Card(String last4, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitCard(this); } }
|
||||
record Upi(String vpa, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitUpi(this); } }
|
||||
record NetBanking(String bank, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitNetBanking(this); } }
|
||||
|
||||
static class Fee implements Visitor<Long> {
|
||||
public Long visitCard(Card c) { return c.amount() * 2 / 100; }
|
||||
public Long visitUpi(Upi u) { return 0L; }
|
||||
public Long visitNetBanking(NetBanking n) { return 15L; }
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Payment[] all = { new Card("4242", 500), new Upi("ankur@bank", 500), new NetBanking("HDFC", 500) };
|
||||
for (Payment p : all) System.out.println(p + " -> fee " + p.accept(new Fee()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
public class WithDefault {
|
||||
sealed interface Payment permits Card, Upi, NetBanking, Wallet {} // Wallet was added later
|
||||
record Card(long amount) implements Payment {}
|
||||
record Upi(long amount) implements Payment {}
|
||||
record NetBanking(long amount) implements Payment {}
|
||||
record Wallet(long amount) implements Payment {}
|
||||
|
||||
// Written back when there were only three kinds. The default keeps it compiling, and keeps it wrong.
|
||||
static long feeWithDefault(Payment p) {
|
||||
return switch (p) {
|
||||
case Card c -> c.amount() * 2 / 100;
|
||||
case Upi u -> 0;
|
||||
default -> 15;
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("fee for Wallet(1000) = " + feeWithDefault(new Wallet(1000)) + " (the rule for Wallet is 1 percent, so 10; it compiled without any complaint)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user