What do we want to achieve?
In the last section we have dealt with the handling of exceptions. The result of this was that we always had a case differentiation. Firstly, the smooth runners and secondly the error case. Here, we were able to formulate elegantly with an Optional<T>
or more functionally with a Result<T>
.
But how do we handle such case differentiations in even more general way? What happens when there are only two ways?
In addition to the source text examples given in this article, I will also use the sources from the Open Source project
Functional-Reactive http://www.functional-reactive.org/. The sources are available at https://github.com/functional-reactive/functional-reactive-lib
classical constructs
if else constructs
In Java language, it is also possible to formulate a case differentiation by means of if(..)
and else
. In the simplest case, we get something as follows.
if(value == null) { /* do something */}
else { /* do something*/ }
One can now also combine the if
` constructs with one another, in order to intercept n-cases.
if (value.equals("a")) { /* do something */}
else if (value.equals("b")) { /* do something */}
else if (value.equals("c")) { /* do something */};
? : Constructs
There is also a different notation that one can use, if one expects a return value. One can combine again the expression itself, in order to take more than two cases into consideration.
String x = /* something */;
String valueA = (x == null) ? "" : x ;
String valueB = (x == null) ? "" : (x.equals("a") ? "A" : "xx");
String valueC = (x == null) ? ""
: (x.equals("a")) ? "A"
: (x.equals("b")) ? "B"
: "xx";
The principal structure is quite simple. An expression is formulated within brackets, which must have a Boolean value as result. If the value is true, the expression given after ? is executed, and if the value is false, the expression given after : is used. Now, the expression given after : can also be a case differentiation. In this way, we can intercept any number of cases one after the other. It is important to understand here that the expression must always consist of two values. That is, one result for true and an explicit result for the case false. In this way, no case can be forgotten, which can happen if one write the if
statement not followed by an else
.
switch constructs
And last but not the least, there is the good old switch
instruction. One can handle multiple cases here too, including a default solution, if none of the explicitly specified values is applicable.
final String x = "A";
switch (x) {
case "A": break;
case "B": break;
case "C": break;
default : break;
}
Considerations
All approaches are based on the fact that a state is evaluated. The formulation is imperative and hence also dependent upon the sequence, in which the checks take place.
All these constructs are designed for the fact that all combinations are already known at the time the source code is being written and we always have the state outside of the constructs to be evaluated. Sometimes, these values, based on which decisions are made, can also be changed.
Case
The aim is that we are able to define the cases independent of one another. The pair always consists of a combination of a condition and the related result. The result can be positive or negative. Control structures should be available, or better, it should be written explicitly, what happens and not how it happens. As the result, a Result<T>
is returned.
Let us first have a look at the formulation of such a pair. To do this, we define a method that expects two parameters. The first parameter is a Supplier<Boolean>
, with which it is signaled, whether the available case is positive or negative. The second parameter is then the result, or better, a Supplier<Result<T>>
.
public static <T> Case<T> matchCase(Supplier<Boolean> condition ,
Supplier<Result<T>> value) {
return new Case<>(condition , value);
}
The return value of this method is an instance of the type Case<T>
.
Case<T> extends Pair<Supplier<Boolean>, Supplier<Result<T>>>
However, the method matchCase(..)
is present in two versions. The second version is for defining the default response. A Supplier<Boolean>
is no longer needed here, because this returns a fixed response indirectly with true. The response of this method is accordingly an instance of the type DefaultCase<T>
public static <T> DefaultCase<T> matchCase(Supplier<Result<T>> value) {
return new DefaultCase<>(() -> true , value);
}
public static class DefaultCase<T> extends Case<T> {
public DefaultCase(final Supplier<Boolean> booleanSupplier ,
final Supplier<Result<T>> resultSupplier) {
super(booleanSupplier , resultSupplier);
}
}
We now come to the usage, or better, to the specification of all cases from the perspective of a developer, who uses this construct.
Integer x = 1;
Result<String> result = Case
.match(
Case.matchCase(() -> Result.success("OK")) ,
Case.matchCase(() -> x == 1 , () -> Result.success("Result 1")) ,
Case.matchCase(() -> x == 2 , () -> Result.success("Result 2")) ,
Case.matchCase(() -> x > 2 , () -> Result.success("Result 3")) ,
Case.matchCase(() -> x < -2 , () -> Result.success("Result 4")) ,
Case.matchCase(() -> false , () -> Result.failure("error message"))
);
result.ifPresentOrElse(
sucess -> { System.out.println("sucess = " + sucess);} ,
failed -> { System.out.println("failed = " + failed);}
);
This can be formulated in a little more compact way with static imports.
match(
matchCase(() -> success("OK")) ,
matchCase(() -> x == 1 , () -> success("Result 1")) ,
matchCase(() -> x == 2 , () -> success("Result 2")) ,
matchCase(() -> x > 2 , () -> success("Result 3")) ,
matchCase(() -> x < -2 , () -> success("Result 4")) ,
matchCase(() -> false , () -> failure("error message"))
)
.ifPresentOrElse(
sucess -> { System.out.println("sucess = " + sucess);} ,
failed -> { System.out.println("failed = " + failed);}
);
How does the processing now take place internally? To do this, we will now have a look at the method match(..)
. Even this is defined as static and expects two parameters. The first is the default Case<T>
and the second is a randomly long list of elements with further instances of the type Case<T>
.
public static <T> Result<T> match(DefaultCase<T> defaultCase ,
Case<T>... matchers) {
return Arrays.stream(matchers)
.filter(m -> m.getT1().get())
.map(m -> m.getT2().get())
.findFirst()
.orElseGet(defaultCase::result);
}
The list of all cases is browsed here at first and in case of the first one, which gives a positive feedback, the related result is fetched. In case none of the cases apply, the default case is used.
Summary
One can now consider at this point that one formulates not just the final values as return value. Rather, at this point, one can also accept a function as the result. One can now start here building up the respective state from various blocks according to a function. An example is given below, which returns a pair based on the input value and a function of the type Function<Integer, Integer>
as the result based on the input value.
Result<Pair<Integer, Function<Integer, Integer>>> result =
match(
matchCase(() -> success(new Pair<>(x , (v) -> v + 1))) ,
matchCase(() -> x == 1 , () -> success(new Pair<>(x , (v) -> v + 1))) ,
matchCase(() -> x == 2 , () -> success(new Pair<>(x , (v) -> v + 2))) ,
matchCase(() -> x > 2 , () -> success(new Pair<>(x , (v) -> v + 3))) ,
matchCase(() -> x < - 2 , () -> success(new Pair<>(x , (v) -> v + 4))) ,
matchCase(() -> false , () -> Result.failure("error message"))
);
result.ifPresentOrElse(
sucess -> { System.out.println("sucess = " + sucess);} ,
failed -> { System.out.println("failed = " + failed);}
);
Similarly, one can also start constructing the streams. There are no limits set to the creativity.
You can find the source code at:
https://github.com/Java-Publications/functional-reactive-with-core-java-006.git
If you have questions or comments, simply contact me at sven@vaadin.com or via Twitter @SvenRuppert.
Happy coding!