Easily combine CompletableFuture
s, List
s, Function
s, Predicate
s and even your own data types!
Check out reading and writing JSON to get a feel for the power of the paradigm.
- Applicatives
Java 8 or higher is required.
Add the required dependencies:
-
nl.wernerdegroot.applicatives.processor:1.2.1
Annotation processor. Only needed during compilation. Hook it into
maven-compiler-plugin
or include it as dependency with scopeprovided
. -
nl.wernerdegroot.applicatives.runtime:1.2.1
Required runtime dependencies. Only a handful of classes, and no transitive dependencies.
Example:
<dependencies>
<dependency>
<groupId>nl.wernerdegroot.applicatives</groupId>
<artifactId>runtime</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>nl.wernerdegroot.applicatives</groupId>
<artifactId>processor</artifactId>
<version>1.2.1</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
You may also want to include prelude
, for applicative instances for some common classes that are included in Java's standard library:
<dependency>
<groupId>nl.wernerdegroot.applicatives</groupId>
<artifactId>prelude</artifactId>
<version>1.2.1</version>
</dependency>
Suppose you have a class like the following:
public class Person {
private final String firstName;
private final String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// Getters, `hashCode`, `equals` and `toString`
}
Let's pretend it will take some time for the application to come up with a firstName
and a lastName
. Perhaps you need to load those from a slow database, or make a network request:
// Get a first name:
String firstName = "Jack";
// Wait a while and get a last name:
TimeUnit.HOURS.sleep(24);
String lastName = "Bauer";
// Combine the first name and last name into a `Person`:
Person person = new Person(firstName, lastName);
Instead of blocking the main thread of your application, you decide to switch to non-blocking CompletableFuture
s. Although it will take some time to obtain firstName
and lastName
and combine those into a Person
, the application is free to perform other, useful tasks while it waits:
// Get a first name:
CompletableFuture<String> futureFirstName =
CompletableFuture.completedFuture("Jack");
// Wait a while and get a last name:
CompletableFuture<String> futureLastName =
CompletableFuture.supplyAsync(() -> {
TimeUnit.HOURS.sleep(24);
return "Bauer";
});
// Combine the two `CompletableFuture`s with a first name
// and a last name into a `CompletableFuture` with a `Person`:
CompletableFuture<Person> futurePerson =
futureFirstName.thenCombine(futureLastName, Person::new);
// Do something useful while we wait for the `Person`.
The method thenCombine
is a nifty function that combines the result of two CompletableFuture
s (by using a BiFunction
that you provide). In this case, the provided BiFunction
is the Person
's constructor that combines a first name and a last name into a Person
object.
The method thenCombine
is very useful, but unfortunately it will only work on two CompletableFuture
s at a time. The next section describes the problem of combining three or four CompletableFuture
s, and shows how to solve this problem using the Java standard library. The solution should leave you somewhat dissatisfied. The section will continue by showing you how you can use this library to reduce the amount of boilerplate to a minimum.
Suppose you work on an application that allows people to trade Pokemon cards online. Such an application might have a PokemonCard
class like the following:
public class PokemonCard {
private final String name;
private final int hp;
private final EnergyType energyType;
private final List<Move> moves;
public PokemonCard(String name, int hp, EnergyType energyType, List<Move> moves) {
this.name = name;
this.hp = hp;
this.energyType = energyType;
this.moves = moves;
}
// Getters, `hashCode`, `equals` and `toString`
}
Imagine that each of the attributes of such a PokemonCard
need to be loaded from some different external system (perhaps your company is using microservices). Instead of having a String
, int
, EnergyType
and List<Move>
, which can be combined directly into a PokemonCard
using the PokemonCard
's constructor, you are stuck with a bunch of CompletableFuture
's that you can't combine directly:
// Fetch the name of the Pokemon:
CompletableFuture<String> futureName = ...;
// Fetch the number of health points:
CompletableFuture<Integer> futureHp = ...;
// Fetch the energy type of the card:
CompletableFuture<EnergyType> futureEnergyType = ...;
// Fetch the moves the Pokemon can perform:
CompletableFuture<List<Move>> futureMoves = ...;
How do you combine those?
Like I claimed in the previous section, thenCombine
won't be of much help. It's capable of combining two CompletableFuture
s, but not any more than that. Unfortunately, the authors of the Java standard library did not provide an overload for thenCombine
that combines four CompletableFuture
s.
If you are willing to go through the hassle, you can still use thenCombine
to combine these four CompletableFuture
s. You'll have to call that method no less than three times: once to combine futureName
and futureHp
into a CompletableFuture
of some intermediate data structure (let's call it NameAndHp
), then again to combine that with futureEnergyType
into a CompletableFuture
of yet another intermediate data structure (named something like NameAndHpAndEnergyType
), and one last time to combine that with futureMoves
into a CompletableFuture
of a PokemonCard
. This is obviously not a solution for a programmer that demands excellence from their programming language!
The best alternative I found is to abandon thenCombine
completely and wait until all CompletableFuture
s are resolved using CompletableFuture.allOf
. We can then use thenApply
and join
to extract the results (which requires some care, as you may accidentally block the main thread if the computation did not complete yet):
CompletableFuture<PokemonCard> futurePokemonCard =
CompletableFuture.allOf(
futureName,
futureHp,
futureEnergyType,
futureMoves
).thenApply(ignored -> {
String name = futureName.join();
int hp = futureHp.join();
EnergyType energyType = futureEnergyType.join();
List<Move> moves = futureMoves.join();
return new PokemonCard(name, hp, energyType, moves);
});
There are several other ways of achieving the same result. See StackOverflow for a discussion about the trade-offs on each of these alternatives.
Instead of having to write all this boilerplate code, wouldn't it be nice if the authors of Java's standard library would just provide a couple of overloads for thenCombine
for three or more CompletableFuture
s? Even though the Java standard library doesn't have such a thing, this library has your back!
All that is required of you is to write a method to combine two CompletableFuture
s, and annotate that with @Covariant
. We write:
public class CompletableFutures {
@Covariant
public <A, B, C> CompletableFuture<C> combine(
CompletableFuture<A> left,
CompletableFuture<B> right,
BiFunction<? super A, ? super B, ? extends C> fn) {
// Implementation already conveniently provided
// by the authors of the Java standard library:
return left.thenCombine(right, fn);
}
}
When you compile, an interface with the name CompletableFuturesOverloads
is generated. It contains many overloads for the combine
-method that accept three or more CompletableFuture
s to combine.
The next step is to modify the CompletableFutures
class and implement this interface:
public class CompletableFutures implements CompletableFuturesOverloads {
@Override
@Covariant
public <A, B, C> CompletableFuture<C> combine(
CompletableFuture<A> left,
CompletableFuture<B> right,
BiFunction<? super A, ? super B, ? extends C> fn) {
// Implementation already conveniently provided
// by the authors of the Java standard library:
return left.thenCombine(right, fn);
}
}
As an (optional) final step, I prefer to add a static instance
-method to classes like this. Such a method is not essential to make the overloads work, but it makes for a pleasant API:
public class CompletableFutures implements CompletableFuturesOverloads {
private static final CompletableFutures INSTANCE = new CompletableFutures();
// Because `CompletableFutures.instance()` reads just a tad nicer
// than `new CompletableFutures()` and provides opportunities to
// reuse the same instance over and over again:
public static CompletableFutures instance() {
return INSTANCE;
}
// Like before...
}
With these overloads in our toolbox, combining four CompletableFuture
s is as easy as combining two:
CompletableFuture<PokemonCard> futurePokemonCard =
CompletableFutures.instance().combine(
futureName,
futureHp,
futureEnergyType,
futureMoves,
PokemonCard::new
);
Note that the CompletableFutures
class as described above is already conveniently included for you in the Prelude module. Check out the implementation and the tests.
In order to test the new Pokemon card application that you and your co-workers are building, it would be helpful to be able to generate a bunch of random PokemonCard
objects to seed the test environment with. One of your co-workers already did much of the hard work, and she wrote the following functions:
// Generate a random name:
Function<Random, String> randomName = ...;
// Generate random health points:
Function<Random, Integer> randomHp = ...;
// Generate a random energy type:
Function<Random, EnergyType> randomEnergyType = ...;
// Generate a random list of moves:
Function<Random, List<Move>> randomMoves = ...;
Note that your co-worker is pretty smart. Each of these generators require an instance of Random
, which guarantees that generating a random String
, Integer
, EnergyType
, or List<Move>
is predictable and repeatable (if you provide it with a predictable and repeatable instance of Random
that is).
All that is left for you is to combine those four separate generators into a generator for PokemonCard
objects (Function<Random, PokemonCard>
). Although the following code works, it is not going to win any beauty contests:
Function<Random, PokemonCard> randomPokemonCard = random -> {
String name = randomName.apply(random);
int hp = randomHp.apply(random);
EnergyType energyType = randomEnergyType.apply(random);
List<Move> moves = randomMoves.apply(random);
return new PokemonCard(name, hp, energyType, moves);
};
There is a more convenient and elegant way to combine these four generators into a generator for PokemonCard
objects. If we write a @Covariant
-annotated method to combine two random generator functions, this library will reward us with a bunch of overloads that combine three or more of those:
public class RandomGeneratorFunctions implements RandomGeneratorFunctionsOverloads {
private static final RandomGeneratorFunctions INSTANCE = new RandomGeneratorFunctions();
public static RandomGeneratorFunctions instance() {
return INSTANCE;
}
@Override
@Covariant
public <A, B, C> Function<Random, C> combine(
Function<Random, A> left,
Function<Random, B> right,
BiFunction<? super A, ? super B, ? extends C> fn) {
return random -> {
A generatedFromLeft = left.apply(random);
B generatedFromRight = right.apply(random);
fn.apply(generatedFromLeft, generatedFromRight);
};
}
}
See how easy it becomes to combine random generator functions?
Function<Random, PokemonCard> randomPokemonCard =
RandomGeneratorFunctions.instance().combine(
randomName,
randomHp,
randomEnergyType,
randomMoves,
PokemonCard::new
);
Note that a class much like the class RandomGeneratorFunctions
described above is already conveniently included for you in the Prelude module. Check out the implementation and the tests.
You will need to write a class that looks this (for a given, imaginary class Foo
):
βββββββββββββββββββββββββββββββββββββββ
β Name of the class is not important. β
ββββββββββββββββββββ¬βββββββββββββββββββ
β βββββββββββββββββββββββββββββββββββββββββββββββ
β β Class can have type parameters. These need β
β β to provided to the generated class as well. β
β βββββββββ¬βββββββββββββββββββββββββββββ¬βββββββββ
βΌ βΌ βΌ
public class CanBeAnything<C1, C2, ..., CN> implements GeneratedClass<C1, C2, ..., CN> {
ββββββββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β Specify name of generated class (optional) β ββββ€ Explained in next section. β
βββββββββββββββββββββββββββββ¬βββββββββββββββββ β ββββββββββββββββββββββββββββββ
β β
@Override βΌ βΌ
@Covariant(className = "GeneratedClass", liftMethodName = "lift", maxArity = 26)
β²
ββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ β
β Method needs exactly three β β Name of the method does β β ββββββββββββββββββββββββ
β type parameters (although β βββββ€ not matter, but overloads β βββ Number of overloads. β
β name is not important). β β β will have the same name. β ββββββββββββββββββββββββ
βββββββββββββββ¬βββββββββββββββ β ββββββββββββββββββββββββββββββ
βΌ βΌ
public <A, B, C> Foo<C> whateverYouLike( βββββββββββββββββββββββββββββ
β² β Typically, the types of β
β β these are identical. In β
Foo<A> left, ββββΌββββββββββββββββββββββββββββββββββββββββββββ€ some cases, the types are β
β β allowed to diverge. See β
Foo<B> right, ββββ β section "Variance". β
βββββββββββββββββββββββββββββ
BiFunction<? super A, ? super B, ? extends C> combinator) {
β²
ββββββββββββββββββββββ β
return ...; ββββ€ This is up to you! β β
ββββββββββββββββββββββ β
} βββββββββββββββββββββ΄ββββββββββββββββββββ
} β Combinator function is always a β
β BiFunction with contravariant β
β parameters and covariant return type. β
βββββββββββββββββββββββββββββββββββββββββ
Foo
can be any data structure for which you can write a class like above. Such data structures are called "applicatives". Common examples from the Java standard library are:
java.util.Optional
java.util.concurrent.CompletableFuture
java.util.function.Function
java.util.function.BiFunction
java.util.List
java.util.Map
java.util.Set
java.util.Stream
java.util.stream.Collector
There are many other data structures like this, such as Mono
/Flux
from Reactor, parser combinators, JSON readers, etc.
Moreover, any "stack" of these data structures (a List
of Optional
s, or a Function
that returns a Stream
of CompletableFuture
s) can automatically be combined this way too! The sections Lift and Stacking describe how stacking of applicatives works.
Note that, in the diagram above, the types of the parameters Foo<A> left
and Foo<B> right
are too strict. Applicatives are typically covariant, and you may want to adjust the types of the parameters to reflect this (use Foo<? extends A>
and Foo<? extends B>
instead of Foo<A>
and Foo<B>
). This is similar to something you'll find in the definition of CompletableFuture.thenCombine
and is generally recommended:
Among programmers, to produce compatible functions, the principle is also known in the form be contravariant in the input type and covariant in the output type.
In certain circumstances, the types of left
and right
are allowed to diverge as well. This is considered to be an advanced feature, but it may be necessary if you want to prevent the execution time overhead of excessive copying. See the implementation of the applicative for List
included in the prelude
module for inspiration.
Lifting is a way to "upgrade" a function that works with regular values like String
s and Integer
s (usually very easy to write) to a similar function that works with, for example, CompletableFuture
s like CompletableFuture<String>
s and CompletableFuture<Integer>
s instead (usually much more tiresome to write).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BiFunction<String, String, Person> β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β CompletableFutures.lift
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BiFunction<CompletableFuture<String>, CompletableFuture<String>, CompletableFuture<Person>> β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Using lift
, you can transform any BiFunction<A, B, C>
into a BiFunction<CompletableFuture<A>, CompletableFuture<B>, CompletableFuture<C>>
or BiFunction<List<A>, List<B>, List<C>>
or whatever applicative you may choose to lift this function into. You are not limited to a BiFunction
either. Any function with up to 26 arguments can be lifted in this fashion.
Let's check out an example:
CompletableFuture<Person> futurePerson =
CompletableFutures.instance().lift(Person::new).apply(
futureFirstName,
futureLastName
);
First we lift the Person
-constructor (a BiFunction<String, String, Person>
) using CompletableFutures
. We immediately invoke it to obtain a CompletableFuture<Person>
.
Of course, the code above could be written more succinctly as:
CompletableFuture<Person> futurePerson =
CompletableFutures.instance().combine(
futureFirstName,
futureLastName,
Person::new
);
So, when would you ever prefer using lift
over calling the combine
-overload that accepts two CompletableFuture
s? This is explained in the next section.
The nice thing about applicatives is that a "stack" of two applicatives is an applicative as well.
Because both CompletableFuture
and List
are applicatives1, their combination is an applicative too. We can combine a CompletableFuture<List<String>>
(first names) and another CompletableFuture<List<String>>
(last names) into a CompletableFuture<List<Person>>
by lifting the combinator Person::new
twice:
// Get a list of first names:
CompletableFuture<List<String>> futureFirstNames =
CompletableFuture.completedFuture(asList("Jack", "Kim"));
// Wait a while and get a list of last names:
CompletableFuture<List<String>> futureLastNames =
CompletableFuture.supplyAsync(() -> {
TimeUnit.HOURS.sleep(24);
return asList("Bauer");
});
// Lift *twice* and then apply. Will yield `new Person("Jack", "Bauer")`
// and `new Person("Kim", "Bauer")` as soon as `futureLastNames` resolves.
CompletableFuture<List<Person>> futurePersons =
CompletableFutures.instance().lift(Lists.instance().lift(Person::new)).apply(
futureFirstNames,
futureLastNames
);
In this example, we are lifting Person::new
twice before we apply:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BiFunction<String, String, Person> β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Lists.lift
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BiFunction<List<String>, List<String>, List<Person>> β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β CompletableFutures.lift
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BiFunction<CompletableFuture<List<String>>, CompletableFuture<List<String>>, CompletableFuture<List<Person>>> β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
If we wanted to, we could even lift the result once more. You can keep stacking applicatives!
After using @Covariant
a couple of times, you are likely wondering if there is a @Contravariant
you could use as well. It turns out there is.
Before we continue, a note of caution may be in order: combining contravariant data types is a bit weird. The rules for such data types are a bit counter-intuitive -- or should I say "contra-intuitive"? -- as the following example shows.
Let's start by composing two Predicate
s by hand. If we have a Predicate
for a Person
's first name and another Predicate
for a Person
's last name, we can combine those into a Predicate
for a Person
:
Predicate<String> isJack = Predicate.isEqual("Jack");
Predicate<String> isBauer = Predicate.isEqual("Bauer");
Predicate<Person> isJackBauer = person -> {
return isJack.test(person.getFirstName()) && isBauer.test(person.getLastName());
};
If we were to write a generic function to combine two Predicate
s, it would look something like this:
public class Predicates {
public <A, B, C> Predicate<C> combine(
Predicate<A> left,
Predicate<B> right,
Function<C, A> extractLeft,
Function<C, B> extractRight) {
return toCheck -> {
return left.test(extractLeft.apply(toCheck)) && right.test(extractRight.apply(toCheck));
};
}
}
When combining two covariant data types, like combining a CompletableFuture<String>
and another CompletableFuture<String>
into a CompletableFuture<Person>
, we had to provide a way to combine a first name (type String
) and last name (also type String
) into a Person
. We used a Person
's constructor for that.
When combining two contravariant data types, like combining a Predicate<String>
and another Predicate<String>
into a Predicate<Person>
, we need to provide a way to split a Person
into a first name (type String
) and a last name (also type String
) into a Person
. This is the exact opposite of what we do for covariant data types!
== Covariance == == Contravariance ==
Combine a first name and a Split a Person into a first
last name into a Person name and a last name
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ
β First name (String) β β Last name (String) β β Person β
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ
β combine β extractRight β β extractLeft
βββββββββββββ¬ββββββββββββ ββββββββββββ ββββββββββββ
βΌ βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ
β Person β β First name (String) β β Last name (String) β
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ
For reasons of performance, we may sometimes need to add an intermediate step between Person
and the two String
s:
== Contravariance ==
Split a Person into a first
name and a last name
βββββββββββββββββββββββ
β Person β
βββββββββββββββββββββββ
β toIntermediate
βΌ
βββββββββββββββββββββββ
β Intermediate β
βββββββββββββββββββββββ
extractRight β β extractLeft
ββββββββββββ ββββββββββββ
βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β First name (String) β β Last name (String) β
βββββββββββββββββββββββ βββββββββββββββββββββββ
It is not important that you understand why, and fortunately it doesn't complicate matters too greatly.
This library can generate a bunch of overloads to combine two or more Predicate
s if we add the following class to our project:
public class Predicates implements PredicatesOverloads {
private static final Predicates INSTANCE = new Predicates();
public static Predicates instance() {
return predicates;
}
@Override
@Contravariant
public <A, B, Intermediate, C> Predicate<C> combine(
Predicate<A> left,
Predicate<B> right,
Function<? super C, ? super Intermediate> toIntermediate,
Function<? super Intermediate, ? extends A> extractLeft,
Function<? super Intermediate, ? extends B> extractRight) {
return (C toCheck) -> {
Intermediate intermediate = toIntermediate.apply(toCheck);
return left.test(extractLeft.apply(intermediate)) && right.test(extractRight.apply(intermediate));
};
}
}
The signature of combine
is a bit more complicated than in the covariant case, but if you take a minute to study this example I'm sure a smart person such as yourself can work it out. Although implementing such a method is a bit complicated, using the generated overloads is pretty straightforward. Here is how we can combine four Predicate
s into a Predicate
for the PokemonCard
class we wrote earlier:
// Verify name:
Predicate<String> isValidName = ...
// Check if sufficiently powerful:
Predicate<Integer> isValidHp = ...
// Ensure that Pokemon is of the right type:
Predicate<EnergyType> isValidEnergyType = ...
// Validate moves:
Predicate<List<Move>> areValidMoves = ...
// Combine:
Predicate<PokemonCard> isValidPokemon =
Predicates.instance().combine(
isValidName,
isValidHp,
isValidEnergyType,
areValidMoves
);
Wait a minute! How does combine
know how to break apart a PokemonCard
?! That will be discussed in the next section.
As noted before, we need to decompose an object into its constituent parts. For a Person
, that would be two String
s. A PokemonCard
decomposes into a String
, Integer
, EnergyType
and a List<Move>
. In order to do so, PokemonCard
implements the Decomposable4<String, Integer, EnergyType, List<Move>>
interface:
public class PokemonCard implements Decomposable4<String, Integer, EnergyType, List<Move>> {
private final String name;
private final int hp;
private final EnergyType energyType;
private final List<Move> moves;
public PokemonCard(String name, int hp, EnergyType energyType, List<Move> moves) {
this.name = name;
this.hp = hp;
this.energyType = energyType;
this.moves = moves;
}
@Override
public <T> T decomposeTo(Function4<? super String, ? super Integer, ? super EnergyType, ? super List<Move>, ? extends T> fn) {
return fn.apply(name, hp, energyType, moves);
}
// Getters, `hashCode`, `equals` and `toString`
}
It's a neat trick that I learned over at Benji Weber's blog. By implementing Decomposable4
or one of its siblings you can decompose objects into basically any other object with similar attributes. If your class implements such an interface, the combine
method is able to take advantage of it by splitting it up into its constituent parts.
If you are using records (and you don't mind a little reflection), you may also want to check out records
. For example:
public record PokemonCard(String name, int hp, EnergyType energyType, List<Move> moves)
implements Record4<String, Integer, EnergyType, List<Move>> { }
If you are unable (or unwilling) to modify your classes to implement an additional interface like Decomposable3
or Record4
, you can always opt for a stand-alone decomposition:
// Break a PokemonCard apart into a Tuple of String, Integer, EnergyType and List<Move>:
Decomposition4<PokemonCard, String, Integer, EnergyType, List<Move>> decomposition =
Decomposition.of(
PokemonCard::getName,
PokemonCard::getHp,
PokemonCard::getEnergyType,
PokemonCard::getMoves
);
Predicate<PokemonCard> isValidPokemon =
Predicates.instance().combine(
isValidName,
isValidHp,
isValidEnergyType,
areValidMoves,
decomposition
);
You can use @Contravariant
for any data structure for which you can write a class like above. Such data structures are called "divisible". Common examples from the Java standard library are:
java.util.Predicate
java.util.concurrent.Comparator
java.util.function.Function
java.util.function.BiFunction
Many of these are included in the prelude
module.
Other examples of contravariant data types that can be combined include Hamcrest's matchers, validators, JSON writers, etc.
It should not be surprising at this point that an @Invariant
annotation is also provided. Perhaps the most obvious application of it is to combine UnaryOperator
s:
public class UnaryOperators implements UnaryOperatorsOverloads {
private static final UnaryOperators INSTANCE = new UnaryOperators();
public static UnaryOperators instance() {
return INSTANCE;
}
@Override
@Invariant
public <A, B, Intermediate, C> UnaryOperator<C> combine(
UnaryOperator<A> left,
UnaryOperator<B> right,
BiFunction<? super A, ? super B, ? extends C> combinator,
Function<? super C, ? extends Intermediate> toIntermediate,
Function<? super Intermediate, ? extends A> extractLeft,
Function<? super Intermediate, ? extends B> extractRight) {
return parameter -> {
Intermediate intermediate = toIntermediate.apply(parameter);
A fromLeft = left.apply(extractLeft.apply(intermediate));
B fromRight = right.apply(extractRight.apply(intermediate));
return combinator.apply(fromLeft, fromRight);
};
}
}
Note that a class is already conveniently included for you in the prelude
module. Check out the implementation and the tests. You may also want to check out JSON readers and writers for another example.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
Footnotes
-
The applicative for lists provides the cartesian product of two lists. β©