In a previous blog post I talked about modelling business rules with types. I introduced a concept called Algebraic Data Types (ADTs). This is a concept I used a lot in 2017/2018 when I was programming in F# a lot – for my day job and side-projects. In F#, ADTs are provided by what are called Discriminated Unions. I haven’t really used F# at all since late 2018. These days I’m mostly using Java, Go, JS and Typescript between work and play. A lot of my day to day work involves microservice development. I find I don’t have too much use for ADTs in this context. Microservices are, by their nature, very thin – usually taking data from somewhere; doing some transformations on it; and sending it on its merry way. Nevertheless, ADTs are still a very useful concept when it comes to domain modelling.
With Java 17 being recently released, I was keen to try out its new features to see if they would give me enough to model data and business rules with ADTs. To give this a go, let’s take what I implemented in that previous post. Let’s see if we can keep our Jazz Bands valid. This time, we’ll use Java 17 instead of Scala!
Okay, so ADTs sound pretty out there! They are actually pretty practical though when it comes to modelling a domain and using the compiler to make illegal states unrepresentable. It’s all about the word “AND” and the word “OR”. A data type can be a “product” type meaning its made of “one thing” AND “another thing” AND “another thing”…..etc. It can also be a “sum” type meaning it’s made up of “one thing” OR “another thing” OR “another thing” …. etc.
Okay, let’s put this into practical terms. Let’s say we have some sort of music application that processes data related to jazz bands. This application is only interested in three different types of jazz band. It wants a jazz band to only ever be either a Piano Trio OR a Quartet OR a Quintet. Also, when we say something is a Piano Trio, we want to be damn sure that it is the type of Piano Trio that the application expects. It wants a Piano Trio to be only made up of a Piano Player AND a Bass Player AND a Drummer. Furthermore, it wants a Quartet to only be made up of a Trumpet Player AND a Piano Player AND a Bass Player AND a Drummer. Finally, it wants a Quintet to be made up of only a Sax Player AND a Trumpet Player AND a Piano Player AND a Bass Player AND a Drummer.
We can model this with some pseudo-code to make the domain a bit clearer:
type PianoTrio = PianoPlayer AND BassPlayer AND Drummer
type Quartet = TrumpetPlayer AND PianoPlayer AND BassPlayer AND Drummer
type Quintet = SaxPlayer AND TrumpetPlayer AND PianoPlayer AND BassPlayer AND Drummer
type JazzBand = PianoTrio OR Quartet OR Quintet
So we can see from the above that each of our JazzBands is a “product” (“AND”) type. Let’s put each one of these together with Java records. For now, we will model each musician in the bands with just a string for their name. We will improve on this later.
public class DomainV1 {
public static record PianoTrio(
String pianoPlayer,
String bassPlayer,
String drummer){}
public static record Quartet(
String trumpetPlayer,
String pianoPlayer,
String bassPlayer,
String drummer){}
public static record Quintet(
String saxPlayer,
String trumpetPlayer,
String pianoPlayer,
String bassPlayer,
String drummer){}
}
Now, let’s say we have a function in our application logic that decides which musician in each type of jazz band is the leader of the band. This function wants to be given a jazz band but it wants to be completely safe in the knowledge that it will only ever be given a properly constructed PianoTrio OR a Quartet OR a Quintet. This sounds like a “sum” (“OR”) type to me. In Java 17, this can be done using a sealed interface. A sealed interface means that the interface can only be implemented by a specified discrete set of types. In our case, we need a jazz band to be modelled using a sealed interface and we want to permit this to only ever be implemented by either a PianoTrio, a Quartet, or a Quintet. This is handled in the following code:
public class DomainV1 {
public sealed interface JazzBand permits PianoTrio, Quartet, Quintet {}
public static record PianoTrio(
String pianoPlayer,
String bassPlayer,
String drummer) implements JazzBand {}
public static record Quartet(
String trumpetPlayer,
String pianoPlayer,
String bassPlayer,
String drummer) implements JazzBand {}
public static record Quintet(
String saxPlayer,
String trumpetPlayer,
String pianoPlayer,
String bassPlayer,
String drummer) implements JazzBand {}
}
We can see that the Java sealed interface, JazzBand, specifies exactly what types it permits to implement it. Now our function to get the leader of a band can accept the JazzBand sealed interface as a parameter and it is completely safe in the knowledge that it will only ever be given a PianoTrio or a Quartet or a Quintet. To implement this function we can take advantage of pattern matching in Java 17. Pattern matching allows us to take a sealed interface type and match on its concrete type. As it is a sealed interface type, the compiler knows which concrete types are permitted. This is available in Java as instanceof. However it doesn’t allow for exhaustive pattern matching (where the compiler tells us if we’ve missed a possible concrete type). So, in the function below, we still need to return something as a default – the empty string in this case.
public class AppV1 {
public static String getBandLeader(DomainV1.JazzBand jazzBand){
if (jazzBand instanceof DomainV1.PianoTrio pianoTrio) {
return pianoTrio.pianoPlayer();
}
if (jazzBand instanceof DomainV1.Quartet quartet) {
return quartet.trumpetPlayer();
}
if (jazzBand instanceof DomainV1.Quintet quintet) {
return quintet.saxPlayer();
}
return "";
}
public static void main(String[] args) {
final var pianoTrio = new DomainV1.PianoTrio(
"Bill Evans",
"Scott LaFaro",
"Paul Motian");
final var quartet = new DomainV1.Quartet(
"Miles Davis",
"Horace Silver",
"Percy Heath",
"Max Roach");
final var quintet = new DomainV1.Quintet(
"Sonny Rollins",
"Kenny Dorham",
"Thelonious Monk",
"Percy Heath",
"Art Blakey");
System.out.println(getBandLeader(pianoTrio));
System.out.println(getBandLeader(quartet));
System.out.println(getBandLeader(quintet));
}
}
instanceof does allow us to bind to a new variable which is automatically casted to the concrete type we are checking on though. For example, when the check for instanceof on PianoTrio is true, we can bind the jazzBand variable to a variable of type PianoTrio, called pianoTrio in the above example. Then we can access fields on the PianoTrio record: pianoTrio.pianoPlayer()
So instanceof with the smart casting that Java provides is nice but it doesn’t really give use full safety. If a new type of JazzBand is created and we don’t handle it, the compiler won’t let us know.
As a preview feature in Java 17, pattern matching on switch has been added. This allows us to do exhaustive checks on the type of our JazzBand. Now we can implement the getBandLeader function as:
public class AppV1 {
public static String getBandLeader(DomainV1.JazzBand jazzBand){
return switch (jazzBand) {
case DomainV1.PianoTrio pianoTrio -> pianoTrio.pianoPlayer();
case DomainV1.Quartet quartet -> quartet.trumpetPlayer();
case DomainV1.Quintet quintet -> quintet.saxPlayer();
};
}
public static void main(String[] args) {
final var pianoTrio = new DomainV1.PianoTrio(
"Bill Evans",
"Scott LaFaro",
"Paul Motian");
final var quartet = new DomainV1.Quartet(
"Miles Davis",
"Horace Silver",
"Percy Heath",
"Max Roach");
final var quintet = new DomainV1.Quintet(
"Sonny Rollins",
"Kenny Dorham",
"Thelonious Monk",
"Percy Heath",
"Art Blakey");
System.out.println(getBandLeader(pianoTrio));
System.out.println(getBandLeader(quartet));
System.out.println(getBandLeader(quintet));
}
}
One thing to notice is that the switch expression is an expression. This means that it evaluates to something. In the above code, it evaluates to a String that we can just return from the function. Pattern matching on switch is similar to pattern matching on instanceof with smart casting. However, it also makes us handle all of the cases. If we leave out a case for one of our concrete JazzBand types, we get a compilation error. So now we can be safe in the knowledge that our getBandLeader function has handled all of the types of JazzBand that exist in our application. The compiler has our backs!
Okay so this is great, we have encoded the constraint that our JazzBands should only ever be a PianoTrio, a Quartet or a Quintet. However, we’re still not fully protected. There are still illegal states that should not happen but that can still happen in our code. Each of our JazzBand concrete types is made up of Strings for each musician. So, there is nothing in the code to stop someone creating a PianoTrio with three drummers!
It’s time for some more types! Instead of passing around a String for a piano player, let’s create a type. We could do this by, again, using a Java record
record PianoPlayer(String name){}
However, this still doesn’t really solve the problem. There is nothing to stop someone creating a PianoPlayer by passing in the name of a bass player! To make sure that we can only create a valid PianoPlayer, we can use a regular Java class, keep the constructor private and provide a static method to return an Optional<PianoPlayer>. If the String that is passed to this method is null or contains the name of someone who is not a piano player, the method will return an empty Optional. Otherwise it will return a PianoPlayer wrapped up in an Optional. The same pattern can be used for the other types of musicians, BassPlayer, TrumpetPlayer, SaxPlayer and Drummer. We will keep things simple and just hard-code the names that are valid for each type of musician. This is all shown below:
public class DomainV2 {
public static class PianoPlayer {
private final String name;
private PianoPlayer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Optional<PianoPlayer> validPianoPlayer(final String name) {
final var validPianoPlayers = Set.of("Bill Evans", "Horace Silver", "Thelonious Monk");
if (name != null && validPianoPlayers.contains(name)) {
return Optional.of(new PianoPlayer(name));
}
return Optional.empty();
}
}
public static class TrumpetPlayer{
private final String name;
private TrumpetPlayer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Optional<TrumpetPlayer> validTrumpetPlayer(final String name) {
final var validTrumpetPlayers = Set.of("Miles Davis", "Kenny Dorham");
if (name != null && validTrumpetPlayers.contains(name)) {
return Optional.of(new TrumpetPlayer(name));
}
return Optional.empty();
}
}
public static class SaxPlayer{
private final String name;
private SaxPlayer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Optional<SaxPlayer> validSaxPlayer(final String name) {
final var validSaxPlayers = Set.of("John Coltrane", "Sonny Rollins");
if (name != null && validSaxPlayers.contains(name)) {
return Optional.of(new SaxPlayer(name));
}
return Optional.empty();
}
}
public static class BassPlayer{
private final String name;
private BassPlayer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Optional<BassPlayer> validBassPlayer(final String name) {
final var validBassPlayers = Set.of("Paul Chambers", "Scott LaFaro", "Percy Heath");
if (name != null && validBassPlayers.contains(name)) {
return Optional.of(new BassPlayer(name));
}
return Optional.empty();
}
}
public static class Drummer {
private final String name;
private Drummer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Optional<Drummer> validDrummer(final String name) {
final var validDrummers =
Set.of("Philly Joe Jones", "Art Blakey", "Paul Motian", "Max Roach");
if (name != null && validDrummers.contains(name)) {
return Optional.of(new Drummer(name));
}
return Optional.empty();
}
}
}
We can take this for a spin with a new version of our App class:
public class AppV2 {
public static String getBandLeader(DomainV2.JazzBand jazzBand){
return switch (jazzBand) {
case DomainV2.Quartet quartet -> quartet.trumpetPlayer().getName();
case DomainV2.PianoTrio pianoTrio -> pianoTrio.pianoPlayer().getName();
case DomainV2.Quintet quintet -> quintet.saxPlayer().getName();
};
}
public static void main(String[] args) {
final var pianoPlayer =
DomainV2.PianoPlayer.validPianoPlayer("Bill Evans")
.orElseThrow(() -> new RuntimeException("invalid piano"));
final var trumpetPlayer = DomainV2.TrumpetPlayer.validTrumpetPlayer("Miles Davis")
.orElseThrow(() -> new RuntimeException("invalid trumpet"));
final var saxPlayer = DomainV2.SaxPlayer.validSaxPlayer("John Coltrane")
.orElseThrow(() -> new RuntimeException("invalid sax"));
final var bassPlayer = DomainV2.BassPlayer.validBassPlayer("Paul Chambers")
.orElseThrow(() -> new RuntimeException("invalid bass"));
final var drummer = DomainV2.Drummer.validDrummer("Philly Joe Jones")
.orElseThrow(() -> new RuntimeException("invalid drummer"));
final var pianoTrio = new DomainV2.PianoTrio(pianoPlayer, bassPlayer, drummer);
final var quartet = new DomainV2.Quartet(trumpetPlayer, pianoPlayer, bassPlayer, drummer);
final var quintet = new DomainV2.Quintet(saxPlayer, trumpetPlayer, pianoPlayer, bassPlayer, drummer);
System.out.println(getBandLeader(pianoTrio));
System.out.println(getBandLeader(quartet));
System.out.println(getBandLeader(quintet));
}
}
Now having to create each valid musician before we can then combine them into a valid band is a little clunky. Using flatMap and map on Optional, we can chain the creation of each musician together and combine them into whatever JazzBand concrete type we are instantiating. For example, to create a valid PianoTrio, we can add a static method onto the PianoTrio type as follows:
public static record PianoTrio(PianoPlayer pianoPlayer,
BassPlayer bassPlayer,
Drummer drummer) implements JazzBand {
public static Optional<PianoTrio> validPianoTrio(String pianoPlayer, String bassPlayer, String drummer) {
return
PianoPlayer.validPianoPlayer(pianoPlayer)
.flatMap(pPlayer -> BassPlayer.validBassPlayer(bassPlayer)
.flatMap(bp -> Drummer.validDrummer(drummer)
.map(d -> new PianoTrio(pPlayer, bp, d))));
}
}
In the above validPianoTrio function, the first musician name that is null or not valid for the corresponding musician will cause an empty Optional to be returned from the whole chain. This in turn will cause an empty Optional to be returned from the function. This is handy because now we have a function that can take musician names as String parameters and it can do all of the validation for us. If the musician names make up a valid PianoTrio, we get back our PianoTrio wrapped up in an Optional. If any of the musicians are not valid, we get an empty Optional back instead.
The same pattern can be used for creating a valid Quartet and a valid Quintet. The only problem with this pattern is that, if we get back an empty Optional from the validPianoTrio function, we don’t know which musician name caused the problem.
We can get around this by introducing a Result type which can either be a Success or Error. This sounds like another sum (“OR”) type to me. Again, we can represent this as a sealed interface that permits 2 concrete types.
public sealed interface Result<T> permits Success, Error {}
public static record Success<T>(T value) implements Result<T> {}
public static record Error<T>(List<String> errors) implements Result<T> {}
The Success case holds a value. The Error case holds a list of String errors. Now we can have a function to create a valid PianoTrio which processes the name Strings. If they all produce a Success for their corresponding musician, then we can create a PianoTrio and return it wrapped in a Success. If one or more of the String names produce an Error for their corresponding musician, then we can aggregate the error Strings and return them wrapped in one aggregated Error. The code for this is below. We take advantage of pattern matching on instanceof here with Java’s smart casting.
public static Result<PianoTrio> validPianoTrioV2(String pianoPlayer,
String bassPlayer,
String drummer) {
final Result<PianoPlayer> pp = PianoPlayer.validPianoPlayerResult(pianoPlayer);
final Result<BassPlayer> bp = BassPlayer.validBassPlayerResult(bassPlayer);
final Result<Drummer> dr = Drummer.validDrummerResult(drummer);
if (pp instanceof Success<PianoPlayer> pPlayer &&
bp instanceof Success<BassPlayer> bPlayer &&
dr instanceof Success<Drummer> drr) {
return new Success<>(new PianoTrio(pPlayer.value, bPlayer.value, drr.value));
}
var errors = new ArrayList<String>();
if (pp instanceof Error<PianoPlayer> ppErrors) {
errors.addAll(ppErrors.errors);
}
if (bp instanceof Error<BassPlayer> bpErrors) {
errors.addAll(bpErrors.errors);
}
if (dr instanceof Error<Drummer> drErrors) {
errors.addAll(drErrors.errors);
}
return new Error<>(errors);
}
The code above is a little long-winded but it does the job and and it is fairly clear what it is doing. This is probably the way I would write this function if I were to write it in practice. There is another way to do this though. It’s a little esoteric though and I probably wouldn’t use it in practice. However, it has been a while since I scratched the Functional Programming itch so lets just go through it for a bit of fun! It uses a concept called the applicative functor. It is a Functional Programming pattern for combining wrapper types such as the Result wrapper type that we have here. Using it, we can chain together Result objects that should hold valid musicians to make up a PianoTrio. If all the musicians in the chain are valid, we get a valid PianoTrio out of the chain. If one or more of the musicians in the chain are not valid, the errors get aggregated along the chain and we get an Error out of the chain with a list of the aggregated error Strings.
First of all, we need a function that takes the following parameters: a Result which wraps a Function which does some sort of transformation on a value; and a Result which wraps a value (in the Success case) which will be passed into the wrapped Function of our first parameter. This is shown below;
public static <T, U> Result<U> resultFMap(Result<Function<T, U>> f, Result<T> v) {
return switch (v) {
case Success<T> s ->
switch (f) {
case Success<Function<T, U>> ss ->
new Success<U>(ss.value().apply(s.value()));
case Error<Function<T, U>> err -> new Error<U>(err.errors());
};
case Error<T> error ->
switch (f) {
case Success<Function<T, U>> ss -> new Error<>(error.errors);
case Error<Function<T, U>> err -> {
final var errors = new ArrayList<String>();
errors.addAll(error.errors);
errors.addAll(err.errors);
yield new Error<U>(errors);
}
};
};
}
We take the v parameter. If it is a Success case, we check the parameter, f. If this is also a Success case, then we can apply the function that it contains to the value that v contains and wrap this up an a Success case. If f is an Error case, then we return a new Error case which wraps the same errors that f wraps.
If the v parameter is an Error case, we check the parameter, f. If f is also an Error case, then we combine the error Strings contained in v and the error Strings contained in f and return a new Error case with the combined error Strings. If f is a Success case, then we return a new Error case which wraps the error Strings that were wrapped by v.
With this resultFMap function in place, we can use it in a function to create a PianoTrio as follows:
public static Result<PianoTrio> validPianoTrioV3(String pianoPlayer,
String bassPlayer,
String drummer) {
final Function<PianoPlayer, Function<BassPlayer, Function<Drummer, PianoTrio>>> curried =
(PianoPlayer pp) -> (BassPlayer bp) -> (Drummer d) -> new PianoTrio(pp, bp, d);
return resultFMap(
resultFMap(
resultFMap(
new Success<>(curried),
PianoPlayer.validPianoPlayerResult(pianoPlayer)),
BassPlayer.validBassPlayerResult(bassPlayer)),
Drummer.validDrummerResult(drummer));
}
}
In this function, we create a local variable called curried. In Functional Programming a curried function is one where a function that takes multiple parameters is broken up into a series of outer to inner functions, each of which takes one parameter. So the curried variable here is equal to a curried version of the PianoTrio constructor.
We can then wrap the curried function in a Success case, new Success<>(curried). Because curried essentially takes 1 parameter and returns another function, we can apply it to a Result<PianoPlayer> type. If the Result<PianoPlayer> is a Success case, the PianoPlayer value it contains is passed in as an argument to curried by way of the resultFMap function we created earlier. If Result<PianoPlayer> is an Error case, then the resultFMap will add its errors Strings to an aggregated list of String errors and return these wrapped in a new Error case. This pattern continues along the chain for a Result<BassPlayer> and a Result<Drummer>. So, at the end of the chain, we return a result which is either a Success case containing a valid PianoTrio or an Error case containing an aggregated list of String errors.
We can take all of this for a spin below:
public static void main(String[] args) {
final var validPianoTrio =
DomainV2.PianoTrio.validPianoTrioV3("Bill Evans", "Percy Heath", "Art Blakey");
final var invalidPianoTrio1 =
DomainV2.PianoTrio.validPianoTrioV3("Art Blakey", "Percy Heath", "Art Blakey");
final var invalidPianoTrio2 =
DomainV2.PianoTrio.validPianoTrioV3("Bill Evans", "Philly Joe Jones", "Art Blakey");
final var invalidPianoTrio3 =
DomainV2.PianoTrio.validPianoTrioV3("Bill Evans", "Percy Heath", "Bill Evans");
final var invalidPianoTrio4 =
DomainV2.PianoTrio.validPianoTrioV3("Bill Evans", "Bill Evans", "Miles Davis");
System.out.println(validPianoTrio);
System.out.println(invalidPianoTrio1);
System.out.println(invalidPianoTrio2);
System.out.println(invalidPianoTrio3);
System.out.println(invalidPianoTrio4);
}
We can also add a toString to each of the musician types e.g.
public String toString() {
return "PianoPlayer{" +
"name='" + name + '\'' +
'}';
}
Now, when we execute the main function above, we get the below output:
Success[value=PianoTrio[pianoPlayer=PianoPlayer{name='Bill Evans'}, bassPlayer=BassPlayer{name='Percy Heath'}, drummer=Drummer{name='Art Blakey'}]]
Error[errors=[invalid piano player Art Blakey]]
Error[errors=[invalid bass player Philly Joe Jones]]
Error[errors=[invalid drummer Bill Evans]]
Error[errors=[invalid drummer Miles Davis, invalid bass player Bill Evans]]
The applicative functor pattern is a nice pattern. However, it is a bit esoteric so I wouldn’t really use it in day-to-day code that much.
Conclusion
This was a fun blog to write. Java 17 has really great features which provide the ability to model domains using ADTs. They also enable some of the more esoteric Functional Programming patterns usually found in languages like Haskell. I’m looking forward to when pattern matching on switch moves out of preview and becomes a main feature of the language. Thanks for reading!