Skip to content

Instantly share code, notes, and snippets.

@jdigger
Last active August 29, 2015 14:14
Show Gist options
  • Save jdigger/2379b590c3f5fb6ed99c to your computer and use it in GitHub Desktop.
Save jdigger/2379b590c3f5fb6ed99c to your computer and use it in GitHub Desktop.
Union Class In Java
import javax.annotation.Nonnull;
/**
* Indicates there was an error.
*/
public interface Failure {
/**
* Returns the error.
*/
@Nonnull
String errorMessage();
}
import javax.annotation.Nullable;
/**
* Indicates that the call was successful.
*/
public interface Success<T> {
/**
* The value of the invocation.
*/
@Nullable
T get();
}
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* An example of creating a "closed union class" in Java.
* <p>
* This technique is useful for any case where you need to use a constrained possible set of types,
* but they don't all inherit from the same hierarchy, so "normal" polymorphism does not apply.
* <p>
* It's a very typical thing to do in environments with a much more robust type system, but can be
* "forced" even in Java, as seen below. See the "main" method for an example of usage.
* <p>
* Inspired by http://www.slideshare.net/ScottWlaschin/fp-patterns-buildstufflt
*/
public class SuccessOrFailure<T> {
private final Object val; // the types don't share a hierarchy, so this is the best we can do...
/**
* We are guaranteeing that this can only be created with a Success or a Failure through the factory methods.
*/
// @SuppressWarnings("ConstantConditions")
private SuccessOrFailure(@Nonnull Object val) {
this.val = val;
}
/**
* Returns a SuccessOrFailure that contains a Success.
*/
@Nonnull
public static <T> SuccessOrFailure<T> success(@Nullable T val) {
// taking advantage of Java 8 lambda support; otherwise can be done
// more clumsily and slowly with an inner class
return new SuccessOrFailure<>(((Success)() -> val));
}
/**
* Returns a SuccessOrFailure that contains a Failure with the given message.
*/
@Nonnull
public static <T> SuccessOrFailure<T> failure(@Nonnull String message) {
return new SuccessOrFailure<>((Failure)() -> message);
}
/**
* Returns a SuccessOrFailure that contains a Failure with the given Throwable.
*/
@Nonnull
public static <T> SuccessOrFailure<T> failure(@Nonnull Throwable throwable) {
return new SuccessOrFailure<>(new ThrowableFailure(throwable));
}
/**
* If this contains a Success, return it. otherwise returns a null
*/
@Nullable
@SuppressWarnings("unchecked")
public Success<T> success() {
return (val instanceof Success) ? (Success<T>)val : null;
}
/**
* If this contains a Failure, return it. otherwise returns a null
*/
@Nullable
@SuppressWarnings("unchecked")
public Failure failure() {
return (val instanceof Failure) ? (Failure)val : null;
}
/**
* Convert a "normal" function to return a SuccessOrFailure.
* The only thing that will generate a Failure is if the function throws an exception.
*
* @param func the function to translate to return SuccessOrFailure
* @param <P> the type of the parameter to the function
* @param <R> the type of the return value
*/
@Nonnull
public static <P, R> Function<P, SuccessOrFailure<R>> bind(Function<P, R> func) {
// prior to Java 8 this can be done with a Callable or the like, but... ugh
return (x) -> {
try {
return SuccessOrFailure.success(func.apply(x));
}
catch (Throwable exp) {
return SuccessOrFailure.failure(exp);
}
};
}
/**
* Merge a pair of "normal" Consumers into a single Consumer that can handle a SuccessOrFailure.
*
* @param successConsumer the Consumer to call if a Success is passed in
* @param failureConsumer the Consumer to call if a Failure is passed in
* @param <P> the type of the parameter for Success
*/
@Nonnull
// @SuppressWarnings("ConstantConditions")
public static <P> Consumer<SuccessOrFailure<P>> merge(Consumer<P> successConsumer, Consumer<Failure> failureConsumer) {
return x -> {
if (x.failure() == null)
successConsumer.accept(x.success().get());
else
failureConsumer.accept(x.failure());
};
}
/**
* Convert a "normal" function to accept a SuccessOrFailure and return a SuccessOrFailure.
* The only thing that will generate a Failure is if the function throws an exception.
* If a Failure is passed in, it is simply returned.
*
* @param func the function to translate to return SuccessOrFailure
* @param <P> the type of the parameter to the function
* @param <R> the type of the return value
*/
@Nonnull
@SuppressWarnings({"ConstantConditions", "unchecked"})
public static <P, R> Function<SuccessOrFailure<P>, SuccessOrFailure<R>> map(Function<P, R> func) {
return (x) -> {
if (x.failure() != null) return (SuccessOrFailure<R>)x;
try {
return SuccessOrFailure.success(func.apply(x.success().get()));
}
catch (Throwable exp) {
return SuccessOrFailure.failure(exp);
}
};
}
/**
* Maps a pair of "normal" Consumers into a Function that can handle a SuccessOrFailure.
* Depending on if Success or Failure is passed in, the appropriate Consumer is called and the
* same SuccessOrFailure is returned to be propagated along the chain.
*
* @param successConsumer the Consumer to call if a Success is passed in
* @param failureConsumer the Consumer to call if a Failure is passed in
* @param <P> the type of the parameter for Success
*/
@Nonnull
@SuppressWarnings({"ConstantConditions", "unchecked"})
public static <P> Function<SuccessOrFailure<P>, SuccessOrFailure<P>> map(Consumer<P> successConsumer, Consumer<Failure> failureConsumer) {
return (x) -> {
Failure failure = x.failure();
if (failure != null)
failureConsumer.accept(failure);
else
successConsumer.accept(x.success().get());
return x;
};
}
@Override
public String toString() {
return (val instanceof Success) ? "Success(" + ((Success)val).get() + ")" : "Failure(" + ((Failure)val).errorMessage() + ")";
}
}
import javax.annotation.Nonnull;
public class ThrowableFailure implements Failure {
private Throwable throwable;
public ThrowableFailure(@Nonnull Throwable throwable) {
this.throwable = throwable;
}
@Nonnull
@Override
public String errorMessage() {
return this.throwable.toString();
}
@SuppressWarnings("UnusedDeclaration")
public Throwable getThrowable() {
return throwable;
}
}
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Date;
import java.util.Random;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.LongStream;
public class TwoTrackTester {
public static void main(String[] args) {
//
// Simple example of making a call that can either succeed or fail
//
SuccessOrFailure<Date> retVal;
// do something successfully
retVal = doSomethingWithFailure(true);
if (retVal.success() != null) {
System.out.println("Success: " + retVal.success().get());
}
// do something but fail
retVal = doSomethingWithFailure(false);
if (retVal.success() == null) {
System.out.println("Failed: " + retVal.failure().errorMessage());
}
//
// This gets even more useful when combined with functional programming...
// Using Java 8 lambdas, because this kind of thing pre-Java8 is a pain.
//
// Create a function that gets the current Date and converts it to a String.
// The "bind" converts a "normal" function to return a SuccessOrFailure.
// The "map" converts a "normal" function to accept a SuccessOrFailure and
// return SuccessOrFailure.
Function<Boolean, SuccessOrFailure<String>> funcDateAsString =
SuccessOrFailure.bind(TwoTrackTester::getDate).
andThen(SuccessOrFailure.map(Date::toString));
Consumer<String> successPrinter = x -> System.out.println("Good: " + x);
Consumer<Failure> failurePrinter = x -> System.out.println("Boom: " + x.errorMessage());
// Add Consumers to the end of the function chain and call by applying some arguments
funcDateAsString.
andThen(SuccessOrFailure.map(successPrinter, failurePrinter)).apply(true);
funcDateAsString.
andThen(SuccessOrFailure.map(successPrinter, failurePrinter)).apply(false);
//
// This is where it really comes into its own:
//
// When the same techniques are applied to a Stream, it allows data to cleanly move from one end of
// the Stream to the other. It's trivial to add validators, enrichers, transformers, wiretaps, etc.
//
// In contrast to an EIP framework (such as Apache Camel or Spring Integration), there's fewer moving parts
// (e.g., Messages, Exchanges, etc.) so it's both more efficient and easier to reason about at the cost of
// not being quite as powerful.
//
// In contrast to reactive programming, this provides a very serial path, which is much easier to
// reason about (and the call stack is actually useful).
//
// Of course it also means that any concurrent execution will consume that thread/process until the
// Stream is finished because it isn't taking advantage of an event reactor/dispatcher.
//
// Use whatever tool works best for the problem at hand.
//
LongStream longStream = new Random().longs();
// only even longs are allowed through (for whatever arbitrary reason :-)
Function<Long, SuccessOrFailure<Long>> validator =
x -> (x % 2 == 0) ? SuccessOrFailure.success(x) : SuccessOrFailure.failure("Could not get an odd value");
longStream.limit(10).parallel().
mapToObj(validator::apply). // validate the data
map(SuccessOrFailure.map((Function<Long, Date>)Date::new)). // transform long to Date
map(SuccessOrFailure.map(Date::toString)). // transform Date to String
peek(x -> System.out.println("Seeing " + x + " pass by")). // put a "wiretap" in to see traffic through the Stream
forEach(SuccessOrFailure.merge(successPrinter, failurePrinter)); // print the results
}
@Nonnull
static Date getDate(boolean shouldSucceed) {
if (shouldSucceed) return new Date();
else throw new IllegalArgumentException("Could not get Date");
}
@Nonnull
static SuccessOrFailure<Date> doSomethingWithFailure(boolean shouldSucceed) {
return (shouldSucceed) ? SuccessOrFailure.success(new Date()) : SuccessOrFailure.failure("Got a failure");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment