Java Try monad using java 17 sealed interfaces
One pattern I find very effective is using sealed interfaces or classes. They work really well in place of Enum because the trait can define an implementation and the compiler can ensure that if a switch (java) or match (scala) statement references the type that every possible type is handled.
Lets look at an implementation of the Try monad that just landed in maven central . For those not familiar with scala's Try.
val dividend = Try(StdIn.readLine("Enter an Int that you'd like to divide:\n").toInt)
val divisor = Try(StdIn.readLine("Enter an Int that you'd like to divide by:\n").toInt)
val problem = dividend.flatMap(x => divisor.map(y => x/y))
problem match {
case Success(v) =>
println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
Success(v)
case Failure(e) =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
println("Info from the exception: " + e.getMessage)
divide
} The first time I say this I didn't think it had much value over the build in try {} catch {} finally {}. One reason things like Try exist is that some languages wish to have less "special forms". A language like LISP or smalltalk you want more of a code-as-data approach. As you know slowly Java has been getting more "functional" things that first appeared in groovy/clojure/scala are working there way into core java.
The code below is here on github
The code idea of the Try monad is that a "block" of code can be either a Success or a Failure. The most simple way to do this is 'always' catch an exception and avoid throwing it to the user :
static <X> Try<X> of(Supplier<X> supplier){
try {
X x = supplier.get();
return new Success<>(x);
} catch (RuntimeException e){
return new Failure<>(e);
}
}
A Success looks like:
public class Success<T> implements Try<T> {
protected final T result;
public Success(T t) {
result = t;
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public T getOrElse(T t) {
return this.result;
}
A Failure like this:
public class Failure<T> implements Try<T> {
protected final Throwable thrown;
public Failure(Throwable t) {
this.thrown = t;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public T get() {
throw new WrappedThrowable(thrown);
}
Sealed classes and interfaces allow you to "lock-down" the sub interfaces. The new syntax since Java 17 is here:
public sealed interface Try<T> extends Product1<T> permits Success, Failure {
Then the children look like this (I:
public non-sealed class Success<T> implements Try<T> {
A sealed class must be final. (I am somewhat anti-final because it makes testing with Mockito hard at times)
The thing that makes sealed interesting is when you combine it with a case statement
Try<Integer> integerTry = Try.of(() -> 4 );
assertInstanceOf(Success.class, integerTry);
switch (integerTry) {
case Failure f -> fail("It should not fail " + f);
case Success<Integer> i -> assertEquals(4, i.get());
}
There isn't a way you can "forget" to code a case the compiler forces you to match them all. Also you dont need a default because the compiler can tell if all the contracts are satisfied.
Where this really shines is places with more than 2-children. For example along time ago we had a Domain Specific Language we used to hide users from Oozie XML. The OozieDSL didnt start knowing everything about oozie. Imagine a codebase with statements like this all over the place.
public void handleJob(JobType t){
if (t instanceof SparkJob) //do this
if (t instanceof HiveJob) //do that
}
If someone adds a new Job Type, they might miss one of the case statements somewhere. Not with sealed traits/interfaces/classes. You wont be able to compile until every case is satisfied.
In places where you can not use sealed interface/class/trait. I have a custom exception class I use in place of IllegalArgumentException.
public void handleJob(JobType t){
if (t instanceof SparkJob) //do this
if (t instanceof HiveJob) //do that
throw new io.teknek.dysfx.exception.UnreachableException("This branch should never be reached. Either you used the library incorrectly or more likely the maintainers borked it. Send us the stack trace!");
}
There you have it! Try out the Try monad and my other dysfx utilities.
One thing to look out for. It is possible in a distributed application that someone could possibly construct a type you dont know about it. Think about this:
V2
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
V3
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
class Pig extends Animal{} //new one
V4
class Animal {}
class Dog extends Animal{}
//class Cat extends Animal{} // removed cat :(
class Pig extends Animal{}
This type of thing is a hard problem, but I thought I would mention it.
Comments
Post a Comment