Enterprise UX
Join our upcoming webinar about enterprise UX! September 28, 2022.
Blog

Functional Reactive with Core Java - Part 03

By  
Sven Ruppert
·
On Apr 19, 2018 7:00:00 AM
·

What do we want to achieve?

In the last section, we have dealt with the class Optional<T> available to us since Java 8. We have seen there that we can replace some control structures through a fluent or functional style. This also enables us to counteract the NullpointerException known everywhere. However, the class Optional<T> is defined as final. Accordingly, an extension is not possible, which can be an obstacle for us in many ways. I was not able to find out, why the class Optional<T> is now defined exactly as final. (But I must admit that I did not spend a lot of time to find this out)

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

Feature Backport

With the version Java 9, the class Optional<T> has been extended by some methods. For instance, by the method ifPresentOrElse(..). It becomes possible here for us to specify a Consumer for the case that a value is present; also, for the case that no value is present, a Runnable can be specified. (By the way, this Runnable is processed in the Common-Fork-And-Join-Pool)

Now there is the one or the other rather classical area, in which a new version of an available JDK is used a little later. If one has just a little more time, one would not like to do without this possibility. So, what now?

The path is the same - to extend the Optional<T> declared as final. We generate an interface with the name Result<T>. We can insert our extensions here, whether new or as backport. Only the interoperability to the classical Optional<T> should be as good as possible.

From optional to the result

We now start extending the Optional<T> bit by bit. What you notice as the first thing is the asymmetric expression at many points. For instance, there is an isPresent(). There is no inverse for this, which then leads again and again to constructs, in which an if statement must be processed with negation. Therefore, the first thing we insert in our case is the inverse method Boolean isAbsent().

Similarly, there is only one void ifPresent(Consumer<T> consumer), which we also extend with void ifAbsent(Runnable action).

Since we want to keep the interoperability to the use of an Optional<T> as simple as possible, there are naturally also the signatures for conversion in both the directions.

  default Optional<T> toOptional() {
    return Optional.ofNullable(get());
  }

  static <T> Result<T> fromOptional(Optional<T> optional) {
    Objects.requireNonNull(optional);
    return ofNullable(optional.get(), "Optional hold a null value");
  }

The conversion in a stream can also be shown quickly.

  default Stream<T> stream() {
    if (!isPresent()) {
      return Stream.empty();
    } else {
      return Stream.of(get());
    }
  }

We now come to the methods, which offer a case differentiation. The aim here is to transform the classical if/else structures. One can see here that the return value is always void. Accordingly, no fluent API style is planned here. The values are consumed terminally. If one wants to process further the values as given below, one must view the methods, such as map() discussed later. But let us remain for the time being with the consuming methods.

  void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction);

  void ifPresentOrElse(Consumer<T> success, Consumer<String> failure);

  void ifPresentOrElseAsync(Consumer<? super T> action, Runnable emptyAction);

  void ifPresentOrElseAsync(Consumer<T> success, Consumer<String> failure);

These methods are present in both the versions of synchronous and asynchronous. When we use the synchronous version, for us it means that the call remains blocked for so long till the value has been finally consumed. But this need not always be the requirement and accordingly one can outsource the task to the Common-Fork-And-Join-Pool. If we now wish to view the implementation, then we leave the interface. The interface Result is implemented at first by the abstract class AbstractResult<T> implements Result<T>. All details are included here, which are stateful for one thing, but the value is now really available as attribute, even if still of the type T. The final implementations are present in two versions. Firstly, the class Success<T> extends AbstractResult<T> and secondly the class Failure<T> extends AbstractResult<T>. These implementations are realized as non-static internal class. The generation of instances is offered in the interface via the static methods static <T> Result<T> success(T value) and static <T> Result<T> failure(String errorMessage).

  static <T> Result<T> failure(String errorMessage) {
    Objects.requireNonNull(errorMessage);
    return new Result.Failure<>(errorMessage);
  }

  static <T> Result<T> success(T value) {
    return new Result.Success<>(value);
  }

The question now arise, why is this distributed like this.

AbstractResult implements Result

In the abstract class, we can place all implementations, which are based on the real attribute, but do not have the expression that a successful result is present or not.

  abstract class AbstractResult<T> implements Result<T> {
    protected final T value;

    public AbstractResult(T value) {
      this.value = value;
    }

    @Override
    public void ifPresent(Consumer<T> consumer) {
      Objects.requireNonNull(consumer);
      if (value != null) consumer.accept(value);
    }

    @Override
    public void ifAbsent(Runnable action) {
      Objects.requireNonNull(action);
      if (value == null) action.run();
    }


    public Boolean isPresent() {
      return (value != null) ? Boolean.TRUE : Boolean.FALSE;
    }

    public Boolean isAbsent() {
      return (value == null) ? Boolean.TRUE : Boolean.FALSE;
    }

    @Override
    public T get() {
      return Objects.requireNonNull(value);
    }

    @Override
    public T getOrElse(Supplier<T> supplier) {
      Objects.requireNonNull(supplier);
      return (value != null) ? value : Objects.requireNonNull(supplier.get());
    }
  }

Success extends AbstractResult

The class Success is a success case i.e., a value is present. An access to the offered Customer can be made here directly and the calls can be carried out with the value available internally. There is only the synchronous and asynchronous implementation here.

  class Success<T> extends AbstractResult<T> {

    public Success(T value) {
      super(value);
    }

    @Override
    public void ifPresentOrElse(Consumer<? super T> action , Runnable emptyAction) {
      action.accept(value);
    }

    @Override
    public void ifPresentOrElse(final Consumer<T> success, final Consumer<String> failure) {
      // TODO check if usefull -> Objects.requireNonNull(value);
      success.accept(value);
    }

    @Override
    public void ifPresentOrElseAsync(Consumer<? super T> action , Runnable emptyAction) {
      CompletableFuture.runAsync(()-> action.accept(value));
    }

    @Override
    public void ifPresentOrElseAsync(Consumer<T> success , Consumer<String> failure) {
      CompletableFuture.runAsync(()-> success.accept(value));
    }

  }

Failure extends AbstractResult

As the name already indicates, this is the mapping of an error case, or better, the case, in which there is no value. Since there is no value, but the type is retained, a different attribute is used, in order to represent a meaningful message. In this case, an attribute of the type String for transporting the message. While calling the methods, one can always access the offered alternative.

class Failure<T> extends AbstractResult<T> {

    private final String errorMessage;

    public Failure(final String errorMessage) {
      super(null);
      this.errorMessage = errorMessage;
    }

    @Override
    public void ifPresentOrElse(Consumer<? super T> action , Runnable emptyAction) {
      emptyAction.run();
    }

    @Override
    public void ifPresentOrElse(final Consumer<T> success, final Consumer<String> failure) {
      failure.accept(errorMessage);
    }

    @Override
    public void ifPresentOrElseAsync(Consumer<? super T> action , Runnable emptyAction) {
      CompletableFuture.runAsync(emptyAction);
    }

    @Override
    public void ifPresentOrElseAsync(Consumer<T> success , Consumer<String> failure) {
      CompletableFuture.runAsync(() -> failure.accept(errorMessage));
    }
  }

combine and combineAsync

Now there is a requirement again and again that a result has to be processed further. To do this, for instance, another value is needed, in order then to be processed together with the result to a further state. We have learned the function BiFunction<A,B, R> till now for processing two values with each other. The first value is the result, the value of the result and the second parameter of the function is the newly added value, with which the result is to be processed further. This then looks like as follows in the signature and the implementation and is already established at the interface level as ** default** implementation.

  default <V, R> Result<R> thenCombine(V value , BiFunction<T, V, Result<R>> func) {
    return func.apply(get() , value);
  }

Naturally, one can offer this also in a blocking way, only the return value becomes here a CompletableFuture<Result<R>>

  default <V, R> CompletableFuture<Result<R>> thenCombineAsync(V value , BiFunction<T, V, Result<R>> func) {
    return CompletableFuture.supplyAsync(() -> func.apply(get() , value));
  }

This gives rise to some quite interesting usage areas. Let us have a look at the following example.

practical use of Result

The first attempt is a static one to the extent that the second value is already fixed.

    final Result<String> stringResult = Result.success("value");

    stringResult
        .thenCombine(100 , (s , integer) -> Result.ofNullable(s + integer))
        .ifPresent(System.out::println);

The example is certainly not complex and also less useful from the result, which is generated here. However, it shows how a further value with the content Result<String> can be processed.

Now this should actually give us more flexibility. For this reason, in our example, the static value of the type Integer is now replaced by a Supplier<T>. Now the value needed for processing is generated or determined only at runtime. We can now respond dynamically to the current state.

For our example, we now define an interface with the name Service and the method doWork(..).

  public interface Service {
    Result<String> doWork(String input);
  }

We keep the implementation for this FunctionalInterface very clear.

    final Service service = input -> (Objects.nonNull(input))
                                            ? Result.success(input.toUpperCase())
                                            : Result.failure("Value was null");

We now simply want to process further the current system state of the JVM with the result and as the aim keep both the values together in an instance of the type Pair.

    final Service service = input -> (Objects.nonNull(input))
                                            ? Result.success(input.toUpperCase())
                                            : Result.failure("Value was null");
    service
        .doWork("Hello World")
        .thenCombine(System::nanoTime ,
                     (BiFunction<String, Supplier<Long>, Result<Pair<String, Long>>>)
                         (s , longSupplier) -> Result.success(new Pair<>(s , longSupplier.get())))
        .ifPresentOrElse(
            value -> System.out.println(" value present = " + value) ,
            errormessage -> System.out.println(" value not present error message is = " + errormessage)
        );

In this simple case, one can also use the method <U> Result<U> map(Function<? super T, ? extends U> mapper). We do not need to get any service or a value from outside. Accordingly, nanoTime() can also be addressed directly.

If our example is implemented with this, it then looks like as follows.

    helloWorld
        .map(s -> new Pair<>(s , System.nanoTime()))
        .ifPresentOrElse(
            value -> System.out.println(" value present = " + value) ,
            errormessage -> System.out.println(" value not present error message is = " + errormessage)
        );

We have now used the method ifPresentOrElse(..) twice. One can naturally also extract this again.

    final Consumer<Result<Pair<String, Long>>> resultConsumer = (result) ->
        result
            .ifPresentOrElse(
                value -> System.out.println(" value present = " + value) ,
                errormessage -> System.out.println(" value not present error message is = " + errormessage));

    resultConsumer.accept(helloWorld.map(s -> new Pair<>(s , System.nanoTime())));

But what can this mean for a project? To do this, we write an example, in which three freely selected transformations are intended for a result. Each steps gets as a further value another instance of the class LocalDateTime.now(). In this way, we can see at the end, in which sequence and when have the respective instances been generated. I have intentionally avoided here some calculations, since it should only symbolically stand for values from different sources. The respective calculation step is made available as function.

  public static interface Service {
    Result<String> doWork(String input);
  }

  //Demo for some Services to call
  public static Supplier<Step001> serviceA() { return () -> new Step001(now());}

  public static Function<Step001, Step002> serviceB() { 
      return (step001) -> new Step002(step001.timestamp01 , 
                                      now());
  }

  public static Function<Step002, Step003> serviceC() { 
      return (step002) -> new Step003(step002.timestamp01 , 
                                      step002.timestamp02 , 
                                      now());
  }

  //Demo for some classes
  public static class Step001 {
    private final LocalDateTime timestamp01;

    public Step001(LocalDateTime timestamp01) {this.timestamp01 = timestamp01;}
  }

  public static class Step002 {
    private final LocalDateTime timestamp01;
    private final LocalDateTime timestamp02;

    public Step002(LocalDateTime timestamp01 , 
                   LocalDateTime timestamp02) {
      this.timestamp01 = timestamp01;
      this.timestamp02 = timestamp02;
    }
  }

  public static class Step003 {
    private final LocalDateTime timestamp01;
    private final LocalDateTime timestamp02;
    private final LocalDateTime timestamp03;

    public Step003(LocalDateTime timestamp01 , 
                   LocalDateTime timestamp02 , 
                   LocalDateTime timestamp03) {
      this.timestamp01 = timestamp01;
      this.timestamp02 = timestamp02;
      this.timestamp03 = timestamp03;
    }

    @Override
    public String toString() {
      return "Step003{" +
             "timestamp01=" + timestamp01 +
             ", timestamp02=" + timestamp02 +
             ", timestamp03=" + timestamp03 +
             '}';
    }
  }

Let us divide the individual things in parts independent of time. The service is made available and initially calculates its value, which it returns as Result<String>.

    // some Service....
    final Service service = input -> (Objects.nonNull(input))
                                     ? Result.success(input.toUpperCase())
                                     : Result.failure("Value was null");

We now call the service and specify a workflow for this return value.

    // service will be invoked
    modifiedWorkflow.apply(service.doWork("Hello World"));

This workflow is generated at a different point of time and is then modified again as follows.

  public static Function<Result<String>, Result<Step003>> workflow = (input) ->
      input
          .or(() -> Result.success("nooop")) // default per demo definition here -> convert failure´s
          .thenCombine(
              serviceA() ,
              (value , supplier) -> Result.success(supplier.get()) // not working with value, to make it simple
          )
          .thenCombine(
              serviceB() ,
              (step001 , fkt) -> Result.success(fkt.apply(step001))
          )
          .thenCombine(
              serviceC() ,
              (step002 , fkt) -> Result.success(fkt.apply(step002))
          );

    final Function<Result<String>, Result<Step003>> modifiedWorkflow = workflow
        .andThen(result -> {
          result.ifPresentOrElse(
              value -> System.out.println(" value present = " + value) ,
              errormessage -> System.out.println(" value not present error message is = " + errormessage)
          );
          return result;
        });

One can see here the possibility of defining parts of the function at an earlier point of time, or also modifying them when needed. We are working here again with functions, which we combine. The functions themselves are stateless and can be generated, when needed.

Summary

We have seen in this section, how with the class Optional<T>, which is available since Java 8 and has been extended in Java 9, we can include functional aspects in our daily work. We have seen, how we can avoid control structures with this and how we can equip ourselves against the classical case of an NPE.

Since the class Optional<T> is defined as final, we cannot customize it to our requirements by means of derivation. For this reason, we have introduced the interface Result<T> and removed with it some discrepancies of the class Optional<T>.

We have introduced not just symmetric methods like ifAbsent(). We have also seen, how we can simply implement a backport from Java 9 to Java 8, even if we still cannot start working with the new JDK.

We have implemented the cooperation with the class Optional<T> through converter methods, which allow a transformation between the two environments at all points of time, and therefore, the integration of the existing APIs is no longer a problem.

However, the big difference lies in the possibility of working further with the value not just by processing further the instant Result<T> with the method map(..), but also enabling the integration of further values at runtime by means of BiFunction as synchronous (combine(..)) and as asynchronous (combineAssync(..)).

You can find the source code at:

https://github.com/Java-Publications/functional-reactive-with-core-java-003.git

If you have questions or comments, simply contact me at sven@vaadin.com or via Twitter @SvenRuppert.

Happy coding!
Sven Ruppert
Sven Ruppert has been coding Java since 1996 and is working as Developer Advocate at Vaadin. He is regularly speaking at Conferences like JavaOne/Jfokus/Devoxx/JavaZone/JavaLand and many more and contributes to IT periodicals, as well as tech portals.
Other posts by Sven Ruppert